CommonsChunkPlugin
CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。
基本配置
{
name: string,
names: string[],
filename: string,
minChunks: number|Infinity|function(module, count) => boolean,
// 在传入 公共chunk(commons chunk) 之前所需要包含的最少数量的 chunks 。
// 数量必须大于等于2,或者少于等于 chunks的数量
// 传入 `Infinity` 会马上生成 公共chunk,但里面没有模块。
// 你可以传入一个 `function` ,以添加定制的逻辑(默认是 chunk 的数量)
chunks: string[],
children: boolean,
deepChildren: boolean,
// 如果设置为 `true`,所有公共 chunk 的后代模块都会被选择
async: boolean|string,
minSize: number,
// 在 公共chunk 被创建立之前,所有 公共模块 (common module) 的最少大小。
}
name
- 可以是已经存在的chunk(一般指入口文件)对应的name,那么就会把公共模块代码合并到这个chunk上;否则,会创建名字为name的commons chunk进行合并
module.exports = {
entry: {
main: './main.js',
vendor: ['vue']
},
...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
}),
]
}
以上会打包出main和vendor两个文件,其中vendor提取了vue代码
module.exports = {
entry: {
main: './main.js',
},
...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
},
}),
]
}
以上会打包出main和vendor两个文件,其中vendor提取了公共包
2. 如果一个数组被传入,这相当于插件针对每个 chunk 名被多次调用
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor','runtime'],
filename: '[name].js'
}),
]
等同于
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: '[name].js'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
filename: '[name].js',
chunks: ['vendor']
}),
]
-
如果该选项被忽略,同时
options.async或者options.children被设置,所有的 chunk 都会被使用,否则options.filename会用于作为 chunk 名。chunks: 通过 chunk name 去选择 chunks 的来源。chunk 必须是公共chunk 的子模块。如果被忽略,所有的入口chunk (entry chunk) 都会被选择。
module.exports = {
entry: {
main: './main.js',
vendor: ['vue']
},
...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
chunks: ['main'],
minChunks: function(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
},
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor', 'common', 'main']
}),
]
}
以上会打包出main,vendor,common和manifest四个文件,其中`vendor`提取了vue代码,`common`包含了一些公共包(除去vue),`manifest`就可以简单理解为模块映射关系的集合
理解vue-cliwebpack默认配置中对于CommonsChunkPlugin的配置:
// webpack.base.conf.js ???
new webpack.optimize.CommonsChunkPlugin('common.js')
// webpack.prod.conf.js
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
// 第四步
// `chunkFilename`: 用来指定异步加载的模块名字,异步加载模块中的共同引用到的模块就会被合并到async中指定名字。
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// 第三步
new webpack.HashedModuleIdsPlugin(),
// 第一步
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// 第二步
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// 第四步
new webpack.optimize.CommonsChunkPlugin({
async: 'vendor-async',
children: true
}),
]
}
分步理解:
-
第一步:提取公共包

-
第二步:提取公共包映射关系 manifest: webpack打包默认模块,通过 manifest,webpack 能够对「你的模块映射到输出 bundle 的过程」保持追踪。
当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。
- 第三步:引入
HashedModuleIdsPlugin固定模块id
为了便于看出效果,我们可以尝试将vue和其他node包分开打包,使用配置如下:
module.exports = {
entry: {
main: './main.js',
// 新增的
vue: ['vue']
},
output: {
filename: '[name][chunkhash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: './',
},
plugins: [
...// 省略了一些配置
// 新增的
new webpack.optimize.CommonsChunkPlugin({
name: 'vue',
minChunks: Infinity,
}),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
},
// 新增的
chunks: ['main'],
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor', 'common', 'main', 'vue']
}),
],
}
在main.js中对依赖包jquery分别进行移除操作,未添加HashedModuleIdsPlugin:

添加了HashedModuleIdsPlugin

因为未添加HashedModuleIdsPlugin时,模块id是根据webpack的解析顺序增量的,如果变换解析顺序,那模块id也会随之改变,所以需要使用HashedModuleIdsPlugin,它是根据模块相对路径生成模块标识,如果模块没有改变,那模块标识也不会改变。
- 第四步:使用
async属性处理,异步加载时提取公共代码:
当我们使用异步加载代码时,而且test1和test2同时使用了第三方包,如jquery如下:
import Vue from 'vue';
const a = 1 + 1;
new Vue({
el: '#app',
data: {
vue_test: 'vue is loaded!'
}
})
import('./test1').then(foo => {
});
import('./test2').then(foo => {
});
此时,如果没有使用async属性,打包结果如下:
使用async属性,打包结果如下:

Webpack4之SplitChunksPlugin
webpack官网:
The CommonsChunkPlugin 已经从 webpack v4 legato 中移除。想要了解在最新版本中如何处理 chunk,请查看 SplitChunksPlugin。
现有CommonsChunkPlugin的问题:CommonsChunkPlugin的思路是基于父子关系的,使得其只能统一抽取到父chunk,造成父chunk过大,不可避免的存在重复引入,引入多余代码。
例如:我们打包多页面时候:
// webpack.js
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
main1: './main.js',
main4: './main4.js',
},
output: {
filename: '[name][chunkhash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: './',
},
plugins: [
new CleanWebpackPlugin(['dist'], { root: process.cwd() }),
new HtmlWebpackPlugin({
filename: 'pagea.html',
template: 'index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunks: ['manifest', 'main1', 'vendor'],
}),
new HtmlWebpackPlugin({
filename: 'pageb.html',
template: 'index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
chunks: ['manifest', 'main4', 'vendor'],
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
},
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
}),
],
}
其中main.js引入了vue和jquery,而main4.js只引入了vue,打包后的结果:
打包后,vender包含了vue和jquery,也就是说当我们在加载pageb的时候,多加载不需要的jquery代码。
SplitChunksPlugin的思路:
引入chunkGroup的概念,在入口chunk和异步chunk中发现被重复使用的模块,将重复模块以vendor-chunk的形式分离出来,也就是vendor-chunk可能有多个,不再受限于所有chunk中都共同存在的模块。

升级了webpack4之后,production模式下,SplitChunksPlugin插件是默认被启用的,默认配置如下:
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
也就是说,默认情况下,webpack会根据下述条件自动进行代码块分割:
- 新代码块可以被共享引用,或者这些模块都是来自node_modules文件夹里面
- 新代码块大于30kb(min+gziped之前的体积)
- 按需加载的代码块,并行请求最大数量应该小于或者等于5
- 初始加载的代码块,并行请求最大数量应该小于或等于3
关于SplitChunksPlugin不再做多的介绍,有需要可以查看没有了CommonsChunkPlugin,咱拿什么来分包(译)
打包后文件分析
为了方便查看,我们用最简单的配置去打包,以下为打包配置:
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
entry: {
main: './main.js',
},
output: {
filename: '[name][chunkhash].js',
path: path.resolve(__dirname, '../dist'),
publicPath: './',
},
plugins: [
new CleanWebpackPlugin(['dist'], { root: process.cwd() }),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
},
}),
new BundleAnalyzerPlugin(),
new webpack.HashedModuleIdsPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
}),
],
}
一般情况
从 main.js 开始看代码
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title></title></head><body><div id=app></div><div id=jq></div><script type=text/javascript src=./manifestce30c729b8270ce6a88d.js></script><script type=text/javascript src=./main189de2d1b19ec3394c54.js></script></body></html>
可以看到,打包后 js 文件的加载顺序是先manifest.js,之后才是main.js,这里先看看 main.js 的内容:
// main.js
webpackJsonp([0],{
/***/ "I6/Z":
/***/ (function(module, exports) {
module.exports = 1;
/***/ }),
/***/ "eitI":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__test2__ = __webpack_require__("I6/Z");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__test2___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__test2__);
const a = 1 + 1;
console.log(__WEBPACK_IMPORTED_MODULE_0__test2___default.a)
/***/ })
},["eitI"]);
删去一些无关代码,首先关注到的是webpackJsonp这个函数,可以看见是不在任何命名空间下的,也就是manifest.js应该定义了一个挂在window下的全局函数main.js往这个函数传入三个参数并调用。
这里先记住两个点:
- 第三个参数的值是eitI,与参数2中的某个方法的键是一致的。
- 一个
exports的函数__webpack_exports__和一个类似require的函数__webpack_require__,这两个应该是模块化的关键。
带着以上疑问,我们去看manifest.js
manifest.js 代码阅读
(function(modules) {
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0, resolves = [], result;
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
if(executeModules) {
for(i=0; i < executeModules.length; i++) {
result = __webpack_require__(executeModules[i]);
}
}
return result;
};
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
})
([]);
-
manifest.js内部是一个IIFE。这个函数会接受一个空数组作为参数,该数组被命名为 modules。果然在 window 上挂了一个名为webpackJsonp的函数。它接受的三个参数,分别名为chunkIds,moreModules,executeModules。对应了 main.js 中调用webpackJsonp时传入的三个参数。看下
webpackJsonp:webpackJsonp 先是for遍历了一次moreModules,将moreModules内的所有方法都存在modules, 也就是自执行函数执行时传入的数组。然后判断
executeModules, 也就是第三个参数是否存在,如存在即执行__webpack_require__方法。 -
installedModules 是一个缓存的容器,如果缓存中有对应的 moduleId,那么直接返回它的 exports,不然就定义并赋值一个吧。
-
__webpack_require__最后的返回值是module.exports。webpack 就是将每一个 js 文件封装成一个函数,每个文件中的 require 方法对应的就是
__webpack_require__,__webpack_require__会根据传入的 moduleId 再去加载对应的代码。
以上面的例子总结,梳理一下打包后代码执行的流程:
- 首先执行
manifest.js,在里面定义了一个webpackJsonp方法; - 执行
main.js:执行webpackJsonp函数,将所有的 moreModules, 也就是每一个依赖的文件存起来。 - 因为传入了第三个参数,所以
eitI作为参数去执行__webpack_require__,__webpack_require__执行了eitI值定义的方法,这个方法中有以I6/Z作为参数去执行__webpack_require__(可以理解为加载了依赖文件test2.js),将依赖文件导出的值作为__webpack_require_函数返回值后供eitI内使用。 - 因为
eitI值定义的方法没有导出值,__webpack_require__返回空对象,然后webpackJsonp方法执行完毕。
总结:
-
首先
manifest.js会定义一个webpackJsonp方法,待其他打包后的文件(也可称为 chunk)调用。 -
当调用 chunk 时,会先将该 chunk 中所有的 moreModules, 也就是每一个依赖的文件也可称为 module存起来。
-
之后通过
executeModules判断这个文件是不是入口文件,决定是否执行第一次__webpack_require__。而__webpack_require__的作用,就是根据这个 module 所 require 的东西,不断递归调用__webpack_require__,__webpack_require_函数返回值后供 require 使用。 -
当然,模块是不会重复加载的,因为
installedModules记录着 module 调用后的 exports 的值,只要命中缓存,就返回对应的值而不会再次调用 module。webpack 打包后的文件,就是通过一个个函数隔离 module 的作用域,以达到不互相污染的目的。
异步加载
这里是demo的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title></title>
</head>
<body>
<div id="app"></div>
<div id="jq"></div>
<button class="btn">load</button>
</body>
</html>
// main.js
const p = document.querySelector('#jq');
const btn = document.querySelector('.btn');
btn.addEventListener('click', function() {
//懒加载 test3.js
require.ensure([], function() {
const data = require('./test4');
p.innerHTML = data;
})
})
// test4.js
const data = 'success!';
module.exports = data;
从上面代码看出,test4是懒加载的,只有在点击了按钮才会加载这部分js代码,看一下打包后的运行效果:

可以看出浏览器一开始只加载了main.js和manifest.js,在点击按钮后,才加载了05d9040b8834b81d1aeb6.js。说明代码是被分割了的,只要当对应的条件触发时,浏览器才会去加载指定的资源。而无论之后我们点击多少次,05d9040b8834b81d1aeb6.js 文件都不会重复加载。所以这里先留下一个问题:如何做到不重复加载?
从 main.js 开始看代码
webpackJsonp([1],{
/***/ "Pmdr":
/***/ (function(module, exports, __webpack_require__) {
const p = document.querySelector('#jq');
const btn = document.querySelector('.btn');
btn.addEventListener('click', function() {
//懒加载 test3.js
__webpack_require__.e/* require.ensure */(0).then((function() {
const data = __webpack_require__("O5aC");
p.innerHTML = data;
}).bind(null, __webpack_require__)).catch(__webpack_require__.oe)
})
/***/ })
},["Pmdr"]);
和上文的一般情况相比,问我们注意到__webpack_require__.e这个方法,传入一个数值之后返回一个promise。这方法当promise决议成功后执行切换文本的逻辑,失败则执行__webpack_require__.oe。
综上,我们希望在manifest.js找到这三个问题的答案:
- 如何做到不重复加载?
__webpack_require__.e方法的逻辑__webpack_require__.oe方法的逻辑
manifest.js 代码阅读
部分截取__webpack_require__.e代码:
var installedModules = {};
// objects to store loaded and loading chunks
var installedChunks = {
2: 0
};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId]; // 1.尚未加载对应模块,undefined
// 10.关于如何做到不重复加载?
// 当再次请求同一文件时,由于对应的 module 已经被加载,因而直接返回一个成功的 promise
if(installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
// a Promise means "currently loading".
// 3.答案:假设网络很差的情况下,我们疯狂点击按钮,为避免浏览器发出若干个请求,通过条件判断都返回同一个 promise,当它决议后,所有挂载在它之上的 then 方法都能得到结果运行下去,相当于构造了一个队列,返回结果后按顺序执行对应方法
if(installedChunkData) {
return installedChunkData[2];
}
// setup Promise in chunk cache
// 2. installedChunkData 与 installedChunks[chunkId] 被重新赋值为一个数组,存放着返回值 promise 的 resolve 与 reject
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
// 问题:为何将数组的第三项赋值为这个 promise呢?
installedChunkData[2] = promise;
// start chunk loading
// 4. 创造一个 script 标签插入头部,加载指定的 js
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.timeout = 120000;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = __webpack_require__.p + "" + chunkId + "" + {"0":"5d9040b8834b81d1aeb6","1":"5f81f82fbaa45b7a1fdd"}[chunkId] + ".js";
var timeout = setTimeout(onScriptComplete, 120000);
// 5.js 文件下载成功之后,先执行内容,再执行 onload 方法
script.onerror = script.onload = onScriptComplete;
function onScriptComplete() {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
// 9. 下载失败,未被赋值0,执行reject
if(chunk !== 0) {
if(chunk) {
chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
}
installedChunks[chunkId] = undefined;
}
};
head.appendChild(script);
return promise;
};
总的来说,就是该方法中接受一个名为 chunkId 的参数,返回一个 promise,解释见注释,按数字顺序阅读。
// 6.执行5f81f82fbaa45b7a1fdd.js内容
webpackJsonp([0],{
/***/ "O5aC":
/***/ (function(module, exports) {
const data = 'success!';
module.exports = data;
/***/ })
});
// 截取部分 webpackJsonp 方法
var resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) { // 查看步骤2,installedChunks[0]就是[resolve, reject]
resolves.push(installedChunks[chunkId][0]);
}
// 7. installedChunks[0]被重新赋值为0
installedChunks[chunkId] = 0;
}
while (resolves.length) {
// 8. 执行resolve
resolves.shift()();
}
__webpack_require__.oe方法
__webpack_require__.oe = function(err) { console.error(err); throw err; };
流程:当异步请求文件发起时,先判断该 chunk 是否已被加载,是的话直接返回一个成功的 promise,让 then 执行的函数 require 对应的 module 即可。不然则构造一个 script 标签加载对应的 chunk,下载成功后挂载该 chunk 内所有的 module。下载失败则打印错误。
作者:Luin
【参考】
知多一点 webpack 的 CommonsChunkPlugin