Webpack CommonsChunkPlugin插件研究

4,055 阅读9分钟

起因

公司的旧项目仍然在使用Webpack3。提取公共代码依然使用的是CommonsChunkPlugin插件,所以需要研究一下CommonsChunkPlugin的用法。

但是官方文档的对于此插件的解释,让我感受不到这个插件的默认行为是什么,只是简单的知道它要做的事情是分离代码块。需要好好研究一番。

搭建最简单实验项目

文件目录如下:

image

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:

image

我们来看看这两个文件下的代码,看看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了,给它一个大家都起的一个名字
    }),
  ],
}

效果如下:

image

形成了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
  }),
]

生成效果如下:

image

就这样,我们成功将业务代码,第三方库代码,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());
})

查看打包效果

我们仍然使用上例中的配置,看一下打包效果:

image

输出了4个Chunk,跟我们预想的不一样,少了一个抽离公共代码的vendor Chunk。我们再看一下打包结果分析图:

image

确实是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分析结果可视化的插件
  ],
}

再看一下可视化的打包结果:

image

得到了我们预想中5个Chunk。其中vendor.js是异步Chunk中的公共代码。

CommmonsChunkPlugin的思路总结

一个CommmonsChunkPlugin对象,会让满足minChunks配置想所设置的条件的模块移到一个新的chunk文件中去。如果你想针对异步Chunk提取公共代码,用async属性替换name。

提取的公共Chunk,是原始Chunk的父亲,它们之间有父子级关系。比如上面的vendor.js是index.js、Greeter、GreeterAgain这三个Chunk的父亲。我们在浏览器使用它们时,加载孩子Chunk的时候,必须先加载父亲Chunk。

结束语

今天的研究就先到这里,代码分割是Webpack的主要特性,也是相对比较复杂的一个技术点,如果要应对复杂庞大的项目,就需要我们对代码分割的配置有更深的理解了。