起因
公司的旧项目仍然在使用Webpack3。提取公共代码依然使用的是CommonsChunkPlugin插件,所以需要研究一下CommonsChunkPlugin的用法。
但是官方文档的对于此插件的解释,让我感受不到这个插件的默认行为是什么,只是简单的知道它要做的事情是分离代码块。需要好好研究一番。
搭建最简单实验项目
文件目录如下:
src文件夹下放源代码,里面放了两个模块index.js和Greeter.js。dist文件夹下放输出文件。再看看webpack.config.js中的配置:
var webpack = require('webpack');
module.exports = {
entry: __dirname + "/src/index.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({}),
],
}
配置好输入输出即可,加上我们的CommonsChunkPlugin插件。
开始实验
成功运行一次CommonsChunkPlugin
运行webpack,报错:You did not specify any valid target chunk settings. (你没有指定任何有效的目标chunk设置)。
好,那我们指定一个,我看name字段代表指定的chunk名:
var webpack = require('webpack');
module.exports = {
entry: __dirname + "/src/index.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'IamChunk',
}),
],
}
仍然报错:Multiple assets emit to the same filename bundle.js。 这是说多个chunk不能放在同一个bundle.js中,既然产生了多个,那指定一个输出文件肯定是不行了,需要生成多个bundle文件,将filename: "bundle.js"改成filename: "[name].js"即可。
我在《Webpack 理解Chunk》解释过Chunk。理解Chunk是理解Webpack的关键。
var webpack = require('webpack');
module.exports = {
entry: __dirname + "/src/index.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "[name].js"//打包后输出文件的文件名
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'IamChunk',
}),
],
}
成功,输出如下,生成了两个chunk,一个main,一个IamChunk:
我们来看看这两个文件下的代码,看看CommonsChunkPlugin帮我们做了什么。
// main.js下的代码
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
const greeter = __webpack_require__(1);
document.querySelector("#root").appendChild(greeter());
/***/ }),
/* 1 */
/***/ (function(module, exports) {
module.exports = function () {
var greet = document.createElement('div');
greet.textContent = "Hi there and greetings!";
return greet;
};
/***/ })
],[0]);
// IamChunk下的代码,我做了相应简化,没有贴出全部代码
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ var parentJsonpFunction = window["webpackJsonp"];
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
...
/******/ return result;
/******/ };
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // objects to store loaded and loading chunks
/******/ var installedChunks = {
/******/ 1: 0
/******/ };
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
...
/******/ return module.exports;
/******/ }
/******/ })
/************************************************************************/
/******/ ([]);
呦吼,main.js下只有我们的源代码了,CommonsChunkPlugin将runtime的的代码打包到了IamChunk这个文件中。
runtime的代码是指浏览器运行时,Webpack用来连接模块化应用程序的所有代码。就是上例中IamChunk中的代码。
将多个entry中引用的共同模块分割出来
CommonsChunkPlugin肯定不止是分离runtime代码这一个功能啊,我猜想CommonsChunkPlugin是这样工作的,将多个chunk中公共的代码,提取到一个chunk中。下面我们就来做这个实验。
在src下新建一个index2.js文件,同样引用Greeter.js。然后配置config,配置两个入口:
var webpack = require('webpack');
module.exports = {
entry: {
index: __dirname + "/src/index.js",
index2: __dirname + "/src/index2.js"
},
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "[name].js"//打包后输出文件的文件名
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', // 我们不管它叫IamChunk了,给它一个大家都起的一个名字
}),
],
}
效果如下:
形成了3 个Chunk,我们分别看一下里面的内容:
// index.js 只有源码中index.js文件的代码,没有了Greeter.js的代码。
// index2.js文件也是同样的,没有了Greeter.js的代码。
// 不用多想,Greeter.js的代码肯定是跑到vendor.js里去了。
webpackJsonp([1],{
/***/ 4:
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_react__ = __webpack_require__(1);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_react___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_react__);
const greeter = __webpack_require__(3);
console.log('11', __WEBPACK_IMPORTED_MODULE_0_react___default.a);
document.querySelector("#root").appendChild(greeter());
/***/ })
},[4]);
// vendor.js
// 果然Greeter.js由于是两个Chunk共同使用的模块,所以被抽离到vender中了。
...
/***/ (function(module, exports) {
module.exports = function () {
var greet = document.createElement('div');
greet.textContent = "Hi there and greetings!";
return greet;
};
/***/ })
多个入口中引用的相同的第三方库,也会被分割出来吗
接下来我又实验了除了我们的业务代码,多个入口同时引用第三方库。CommonsChunkPlugin对第三方库并没有什么特殊待遇,如:只有index.js引用了第三方库,index2.js没有引用此第三方库,那么第三方库就会打包到index.js的Chunk里,如果index.js,index2.js都引用了同一个第三方库,那么这个第三方库,就会被打包到vendor.js中。
CommonsChunkPlugin 初印象总结
在我们只配置了name这一个属性的时候,也就是指定公共Chunk名称之后,CommonsChunkPlugin的默认行为是:生成一个公共Chunk,其他多个Chunk同时引用同一个module时,将其提取到这个公共Chunk中。额外的,还会将Webpack runtime的代码提取到这个公共Chunk中。
理解CommonsChunkPlugin的默认行为,是理解它的第一步,我觉得这是官方文档没有好好交代的一个问题。
实战1
初步认识CommonsChunkPlugin之后,我们就要探讨它在日常工作中的应用了。
场景
开发单页应用,一般只有一个入口,代码分割其中的一个用途是,将业务代码(经常变动)和第三方库代码(不经常变动),打包到不同的Chunk中。
分割第三方库
目前第三方库代码只被我们的入口Chunk引用,没有被多个Chunk引用,CommonsChunkPlugin的默认行为不管用了,可是我们仍想将第三方库提取到公共Chunk中
我们将index2从entry中删掉,然后在index.js源代码中,引入react库。
var webpack = require('webpack');
module.exports = {
entry: {
index: __dirname + "/src/index.js",
},
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "[name].[chunkhash].js"//打包后输出文件的文件名
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
}),
],
}
如上的配置,Webpack不会帮咱把react打包到vendor中,毕竟react只被一个chunk引用了。我们需要使用minChunks字段:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
return module.context && module.context.includes("node_modules");
}
}),
],
这里minChunks传入的是一个函数,结果返回true,就是要提取的代码模块,我们将node_modules下的代码,打包时都提取到公共Chunk中。
runtime代码,单独提取
runtime中的manifest代码,每次打包,都有可能变动,这样就违背了我们打包第三方库代码的初衷:利用浏览器缓存,让不变的代码一直可以利用缓存加载。
所以我们要将其单独提取到一个Chunk中,配置如下:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
minChunks: function(module){
return module.context && module.context.includes("node_modules");
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
]
生成效果如下:
就这样,我们成功将业务代码,第三方库代码,runtime代码分别打包到三个Chunk中。这样的配置,能满足我们一般的打包场景了。
minChunks的Infinity分析
这里的minChunks使用了Infinity,这个Infinity代表着立即打包,立即打包可不就只能打包到runtime的内容吗,别的什么也打包不进来啊。
对,这就是Infinity的效果,CommonsChunkPlugin的name还有一种用法,就是跟entry的key一致,如下:
module.exports = {
entry: {
greeter: ['./src/Greeter.js', './src/GteeterAgain.js'],
},
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "[name].[chunkhash].js"//打包后输出文件的文件名
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "greeter",
minChunks: Infinity
}),
],
}
这里面CommonsChunkPlugin的name和entry中的greeter一致,那么插件就立马将Greeter.js,GteeterAgain.js打包起来,不会因为你又搞了什么公共模块,继续往这个greeter的Chunk里添加新模块。
这也就是我说的要先理解CommonsChunkPlugin默认行为。它默认会打包公共模块,所以就有了minChunks: Infinity来说明我这个打包不想打包公共的,只想打包我在entry中指定的。
我们看一下minChunks的签名:
minChunks: number|Infinity|function(module, count) -> boolean,
minChunks还可以传递数字,数字就相对好理解一点,就是要被多个的Chunk同时引用,才会被打包到公共Chunk中。
实战2
场景
现在我们考虑一个相对复杂的场景,就是项目需要使用懒加载。也就是有了异步Chunk。
异步引用模块
我们在index.js中,这样引用Greeter.js、GreeterAgain.js,在Greeter.js、GreeterAgain.js中都引用了react库,我们仍用上例中的配置,推测应该产生5个Chunk:入口Chunk、两个异步Chunk、manifest Chunk、vendor Chunk:
// 懒加载引用,这样就成了异步引用了
import(/* webpackChunkName: "Greeter" */'./Greeter').then(module => {
const greeter = module.default
document.querySelector("#root").appendChild(greeter());
})
import(/* webpackChunkName: "GreeterAgain" */'./GreeterAgain').then(module => {
const greeter = module.default
document.querySelector("#root").appendChild(greeter());
})
查看打包效果
我们仍然使用上例中的配置,看一下打包效果:
输出了4个Chunk,跟我们预想的不一样,少了一个抽离公共代码的vendor Chunk。我们再看一下打包结果分析图:
确实是4个,而且在Greeter和GreeterAgain这两个Chunk中,同时引用了react,这显然不是我们想看到的,我们希望公共代码都抽离出来,哪怕它是异步的Chunk。
抽离异步Chunk中的公共代码
配置如下: 这里用到了async属性,这个属性要替代name属性,告诉Webpack,我这个是针对异步的公共Chunk的名称。
var webpack = require('webpack');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
entry: {
index: __dirname + "/src/index.js",
},
output: {
path: __dirname + "/dist", //打包后的文件存放的地方
filename: "[name].[chunkhash:8].js", //打包后输出文件的文件名
chunkFilename: "[name].[chunkhash:8].js",
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
async: "vendor",
minChunks: function (module) {
return module.context && module.context.includes("node_modules");
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
new BundleAnalyzerPlugin(), // 用于显示Bundle分析结果可视化的插件
],
}
再看一下可视化的打包结果:
得到了我们预想中5个Chunk。其中vendor.js是异步Chunk中的公共代码。
CommmonsChunkPlugin的思路总结
一个CommmonsChunkPlugin对象,会让满足minChunks配置想所设置的条件的模块移到一个新的chunk文件中去。如果你想针对异步Chunk提取公共代码,用async属性替换name。
提取的公共Chunk,是原始Chunk的父亲,它们之间有父子级关系。比如上面的vendor.js是index.js、Greeter、GreeterAgain这三个Chunk的父亲。我们在浏览器使用它们时,加载孩子Chunk的时候,必须先加载父亲Chunk。
结束语
今天的研究就先到这里,代码分割是Webpack的主要特性,也是相对比较复杂的一个技术点,如果要应对复杂庞大的项目,就需要我们对代码分割的配置有更深的理解了。