正文
-
因为两入口都是用到了 utilB,我们希望把它抽离成单独文件,并且当用户访问 pageA 和 pageB 的时候都能去加载 utilB 这个公共模块,而不是存在于各自的入口文件中。
-
pageB 中 utilC 不是页面一开始加载时候就需要的内容,假如 utilC 很大,我们不希望页面加载时就直接加载 utilC,而是当用户达到某种条件(如:点击按钮)才去异步加载 utilC,这时候我们需要将 utilC 抽离成单独文件,当用户需要的时候再去加载该文件。
那么 webpack 需要怎么配置呢?
// 通过 config/webpack.config.multiple.js 打包
const webpack = require('webpack');
const path = require('path')
module.exports = {
entry: {
pageA: [path.resolve(__dirname, '../src/multiple/pageA.js')],
pageB: path.resolve(__dirname, '../src/multiple/pageB.js'),
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[chunkhash:8].js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: 2,
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
]
}
单单配置多 entry 是不够的,这样只会生成两个 bundle 文件,将 pageA 和 pageB 所需要的内容全部放入,跟单入口文件并没有区别,要做到代码切割,我们需要借助 webpack 内置的插件 CommonsChunkPlugin。
首先 webpack 执行存在一部分运行时代码,即一部分初始化的工作,就像之前单文件中的
__webpack_require__
,这部分代码需要加载于所有文件之前,相当于初始化工作,少了这部分初始化代码,后面加载过来的代码就无法识别并工作了。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: 2,
})
这段代码的含义是,在这些入口文件中,找到那些引用两次的模块(如:utilB),帮我抽离成一个叫 vendor 文件,此时那部分初始化工作的代码会被抽离到 vendor 文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor'],
// minChunks: Infinity // 可写可不写
})
这段代码的含义是在 vendor 文件中帮我把初始化代码抽离到 mainifest 文件中,此时 vendor 文件中就只剩下 utilB 这个模块了。你可能会好奇为什么要这么做?
因为这样可以给 vendor 生成稳定的 hash 值,每次修改业务代码(pageA),这段初始化时代码就会发生变化,那么如果将这段初始化代码放在 vendor 文件中的话,每次都会生成新的 vendor.xxxx.js,这样不利于持久化缓存,如果不理解也没关系,下次我会另外写一篇文章来讲述这部分内容。
另外 webpack 默认会抽离异步加载的代码,这个不需要你做额外的配置,pageB 中异步加载的 utilC 文件会直接抽离为 chunk.xxxx.js 文件。
所以这时候我们页面加载文件的顺序就会变成:
mainifest.xxxx.js // 初始化代码
vendor.xxxx.js // pageA 和 pageB 共同用到的模块,抽离
pageX.xxxx.js // 业务代码
当 pageB 需要 utilC 时候则异步加载 utilC
执行
npm run build:multiple
即可查看打包内容,首先来看下 manifest 如何做初始化工作(精简版)?
// dist/mainifest.xxxx.js
(function(modules) {
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
var moduleId, chunkId, i = 0, callbacks = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId])
callbacks.push.apply(callbacks, installedChunks[chunkId]);
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
while(callbacks.length)
callbacks.shift().call(null, __webpack_require__);
if(moreModules[0]) {
installedModules[0] = 0;
return __webpack_require__(0);
}
};
var installedModules = {};
var installedChunks = {
4:0
};
function __webpack_require__(moduleId) {
// 和单文件一致
}
__webpack_require__.e = function requireEnsure(chunkId, callback) {
if(installedChunks[chunkId] === 0)
return callback.call(null, __webpack_require__);
if(installedChunks[chunkId] !== undefined) {
installedChunks[chunkId].push(callback);
} else {
installedChunks[chunkId] = [callback];
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";
head.appendChild(script);
}
};
})([]);
与单文件内容一致,定义了一个自执行函数,因为它不包含任何模块,所以传入一个空数组。除了定义了
__webpack_require__
,还另外定义了两个函数用来进行加载模块。
首先讲解代码前需要理解两个概念,分别是 module 和 chunk
-
chunk 代表生成后 js 文件,一个 chunkId 对应一个打包好的 js 文件(一共五个),从这段代码可以看出,manifest 的 chunkId 为 4,并且从代码中还可以看到:0-3 分别对应 pageA, pageB, 异步 utilC, vendor 公共模块文件,这也就是我们为什么不能将这段代码放在 vendor 的原因,因为文件的 hash 值会变。内容变了,vendor 生成的 hash 值也就变了。
-
module 对应着模块,可以简单理解为打包前每个 js 文件对应一个模块,也就是之前
__webpack_require__
加载的模块,同样的使用数组下标作为 moduleId 且是唯一不重复的。
那么为什么要区分 chunk 和 module 呢?