简介
CommonsChunkPlugin
主要是用来提取第三方库和公共模块,避免首屏加载的bundle
文件或者按需加载的bundle
文件体积过大,从而导致加载时间过长,着实是优化的一把利器。
先来说一下各种教程以及文档中CommonsChunkPlugin
提及到chunk
有哪几种,主要有以下三种:
-
webpack
当中配置的入口文件(entry)
是chunk
,可以理解为entry chunk
- 入口文件以及它的依赖文件通过
code split
(代码分割)出来的也是chunk
,可以理解为children chunk
- 通过
CommonsChunkPlugin
创建出来的文件也是chunk
,可以理解为commons chunk
CommonsChunkPlugin可配置的属性:
-
name
:可以是已经存在的chunk
(一般指入口文件)对应的name
,那么就会把公共模块代码合并到这个chunk
上;否则,会创建名字为name
的commons chunk
进行合并
* filename
:指定commons chunk
的文件名
* chunks
:指定source chunk
,即指定从哪些chunk
当中去找公共模块,省略该选项的时候,默认就是entry chunks
* minChunks
:既可以是数字,也可以是函数,还可以是Infinity
,具体用法和区别下面会说
children
和async
属于异步中的应用,放在了最后讲解。
可能这么说,大家会云里雾里,下面用demo
来检验上面的属性。
实战应用
以下几个demo
主要是测试以下几种情况:
- 不分离出第三方库和自定义公共模块
- 分离出第三方库、自定义公共模块、webpack运行文件,但它们在同一个文件中
- 单独分离第三方库、自定义公共模块、webpack运行文件,各自在不同文件
不分离出第三方库和自定义公共模块
项目初始结构,后面打包后会生成dist
目录:
src目录下各个文件内容都很简洁的,如下:
common.js
export const common = 'common file';
first.js
import {common} from './common';
import $ from 'jquery';
console.log($,`first ${common}`);
second.js
import {common} from './common';
import $ from 'jquery';
console.log($,`second ${common}`);
package.json文件:
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rimraf dist && webpack"
},
"author": "",
"license": "ISC",
"devDependencies": {
"rimraf": "^2.6.2",
"webpack": "^3.10.0",
"webpack-dev-server": "^2.10.1"
},
"dependencies": {
"jquery": "^3.2.1"
}
}
webpack.config.js:
const path = require("path");
const webpack = require("webpack");
const config = {
entry: {
first: './src/first.js',
second: './src/second.js'
},
output: {
path: path.resolve(__dirname,'./dist'),
filename: '[name].js'
},
}
module.exports = config;
接着在命令行npm run build
,此时项目中多了dist
目录:
再来查看一下命令行中webpack
的打包信息:
查看first.js
和second.js
,会发现共同引用的common.js
文件和jquery
都被打包进去了,这肯定不合理,公共模块重复打包,体积过大。
分离出第三方库、自定义公共模块、webpack运行文件
这时候修改webpack.config.js
新增一个入口文件vendor
和CommonsChunkPlugin
插件进行公共模块的提取:
const path = require("path");
const webpack = require("webpack");
const packagejson = require("./package.json");
const config = {
entry: {
first: './src/first.js',
second: './src/second.js',
vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
},
output: {
path: path.resolve(__dirname,'./dist'),
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: '[name].js'
}),
]
}
module.exports = config;
查看dist
目录下,新增了一个vendor.js
的文件:
再来查看一下命令行中webpack
的打包信息:
通过查看vendor.js
文件,发现first.js
和second.js
文件中依赖的jquery
和common.js
都被打包进vendor.js
中,同时还有webpack
的运行文件。总的来说,我们初步的目的达到,提取公共模块,但是它们都在同一个文件中。
到这里,肯定有人希望自家的vendor.js
纯白无瑕,只包含第三方库,不包含自定义的公共模块和webpack
运行文件,又或者希望包含第三方库和公共模块,不包含webpack
运行文件。
其实,这种想法是对,特别是分离出webpack
运行文件,因为每次打包webpack
运行文件都会变,如果你不分离出webpack
运行文件,每次打包生成vendor.js
对应的哈希值都会变化,导致vendor.js
改变,但实际上你的第三方库其实是没有变,然而浏览器会认为你原来缓存的vendor.js
就失效,要重新去服务器中获取,其实只是webpack
运行文件变化而已,就要人家重新加载,好冤啊~
OK,接下来就针对这种情况来测试。
单独分离出第三方库、自定义公共模块、webpack
运行文件
这里我们分两步走:
先单独抽离出webpack
运行文件
接着单独抽离第三方库和自定义公共模块,这里利用minChunks
有两种方法可以完成,往后看就知道了
1、抽离webpack运行文件
这里解释一下什么是webpack
运行文件:
/******/ (function(modules) { // webpackBootstrap
/******/ // install a JSONP callback for chunk loading
/******/ var parentJsonpFunction = window["webpackJsonp"];
/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [], result;
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(installedChunks[chunkId]) {
/******/ resolves.push(installedChunks[chunkId][0]);
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/ for(moduleId in moreModules) {
/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ modules[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/ while(resolves.length) {
/******/ resolves.shift()();
/******/ }
/******/ if(executeModules) {
/******/ for(i=0; i < executeModules.length; i++) {
/******/ result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ }
/******/ }
/******/ return result;
/******/ };
/******/
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // objects to store loaded and loading chunks
/******/ var installedChunks = {
/******/ 5: 0
/******/ };
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ 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;
/******/ }
/******/
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData === 0) {
/******/ return new Promise(function(resolve) { resolve(); });
/******/ }
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ return installedChunkData[2];
/******/ }
/******/
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ });
/******/ installedChunkData[2] = promise;
/******/
/******/ // start chunk loading
/******/ 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 + "static/js/" + ({"3":"comC"}[chunkId]||chunkId) + "." + chunkId + "." + {"0":"3c977d2f8616250b1d4b","3":"c00ef08d6ccd41134800","4":"d978dc43548bed8136cb"}[chunkId] + ".js";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var chunk = installedChunks[chunkId];
/******/ if(chunk !== 0) {
/******/ if(chunk) {
/******/ chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
/******/ }
/******/ installedChunks[chunkId] = undefined;
/******/ }
/******/ };
/******/ head.appendChild(script);
/******/
/******/ return promise;
/******/ };
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/";
/******/
/******/ // on error function for async loading
/******/ __webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/ })
/************************************************************************/
/******/ ([]);
上面就是抽离出来的webpack
运行时代码,其实这里,webpack
帮我们定义了一个webpack\_require
的加载模块的方法,而manifest
模块数据集合就是对应代码中的 installedModules
。每当我们在main.js
入口文件引入一模块,installModules
就会发生变化,当我们页面点击跳转,加载对应模块就是通过\_\_webpack\_require\_\_
方法在installModules
中找对应模块信息,进行加载
参考:https://www.jianshu.com/p/95752b101582
先来抽离webpack
运行文件,修改webpack
配置文件:
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']
}),
]
上面两段抽离webpack
运行文件代码的意思是创建一个名为runtime
的commons chunk
进行webpack
运行文件的抽离,其中source chunks
是vendor.js
。
查看dist
目录下,新增了一个runtime.js
的文件,其实就是webpack
的运行文件:
再来查看一下命令行中webpack
的打包信息,你会发现vendor.js
的体积已经减小,说明已经把webpack
运行文件提取出来了:
可是,vendor.js
中还有自定义的公共模块common.js
,人家只想vendor.js
拥有项目依赖的第三方库而已(这里是jquery
),这个时候把minChunks
这个属性引进来。
minChunks
可以设置为数字、函数和Infinity
,默认值是2,并不是官方文档说的入口文件的数量,下面解释下minChunks
含义:
- 数字:模块被多少个
chunk
公共引用才被抽取出来成为commons chunk
- 函数:接受 (
module, count
) 两个参数,返回一个布尔值,你可以在函数内进行你规定好的逻辑来决定某个模块是否提取成为commons chunk
-
Infinity
:只有当入口文件(entry chunks
) >= 3 才生效,用来在第三方库中分离自定义的公共模块
2、抽离第三方库和自定义公共模块
要在vendor.js
中把第三方库单独抽离出来,上面也说到了有两种方法。
第一种方法minChunks
设为Infinity
,修改webpack
配置文件如下:
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor','runtime'],
filename: '[name].js',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'common',
filename: '[name].js',
chunks: ['first','second']//从first.js和second.js中抽取commons chunk
}),
]
查看dist
目录下,新增了一个common.js
的文件:
再来查看一下命令行中webpack
的打包信息,自定义的公共模块分离出来:
这时候的vendor.js
就纯白无瑕,只包含第三方库文件,common.js
就是自定义的公共模块,runtime.js
就是webpack
的运行文件。
第二种方法把它们分离开来,就是利用minChunks
作为函数的时候,说一下minChunks
作为函数两个参数的含义:
-
module
:当前chunk
及其包含的模块 -
count
:当前chunk
及其包含的模块被引用的次数
minChunks
作为函数会遍历每一个入口文件及其依赖的模块,返回一个布尔值,为true
代表当前正在处理的文件(module.resource
)合并到commons chunk
中,为false
则不合并。
继续修改我们的webpack
配置文件,把vendor
入口文件注释掉,用minChunks
作为函数实现vendor
只包含第三方库,达到和上面一样的效果:
const config = {
entry: {
first: './src/first.js',
second: './src/second.js',
//vendor: Object.keys(packagejson.dependencies)//获取生产环境依赖的库
},
output: {
path: path.resolve(__dirname,'./dist'),
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: '[name].js',
minChunks: function (module,count) {
console.log(module.resource,`引用次数${count}`);
//"有正在处理文件" + "这个文件是 .js 后缀" + "这个文件是在 node_modules 中"
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(path.join(__dirname, './node_modules')) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime',
filename: '[name].js',
chunks: ['vendor']
}),
]
}
上面的代码其实就是生成一个叫做vendor
的commons chunk
,那么有哪些模块会被加入到vendor
中呢?就对入口文件及其依赖的模块进行遍历,如果该模块是js
文件并且在node_modules
中,就会加入到vendor
当中,其实这也是一种让vendor
只保留第三方库的办法。
再来查看一下命令行中webpack
的打包信息:
你会发现,和上面minChunks
设为Infinity
的结果是一致的。
children和async属性
这两个属性主要是在code split
(代码分割)和异步加载当中应用。
-
children
- 指定为
true
的时候,就代表source chunks
是通过entry chunks
(入口文件)进行code split
出来的children chunks
-
children
和chunks
不能同时设置,因为它们都是指定source chunks
的 -
children
可以用来把entry chunk
创建的children chunks
的共用模块合并到自身,但这会导致初始加载时间较长
- 指定为
* async
:即解决children:true
时合并到entry chunks
自身时初始加载时间过长的问题。async
设为true
时,commons chunk
将不会合并到自身,而是使用一个新的异步的commons chunk
。当这个children chunk
被下载时,自动并行下载该commons chunk
修改webpack
配置文件,增加chunkFilename
,如下:
output: {
...........
chunkFilename: "[name].[hash:5].chunk.js",
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor','runtime'],
filename: '[name].js',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
children: true,
async: 'children-async'
})
]
chunkFilename
用来指定异步加载的模块名字,异步加载模块中的共同引用到的模块就会被合并到async
中指定名字,上面就是children-async
。
修改成异步截图出来太麻烦了,就简单说明一下:first
和second
是异步加载模块,同时它们共同引用了common.js
这个模块,如果你不设置这一步:
new webpack.optimize.CommonsChunkPlugin({
children: true,
async: 'children-async'
})
那么共同引用的common.js
都被打包进各自的模块当中,就重复打包了。
OK,你设置之后,也得看children
的脸色怎么来划分:
-
children
为true
,共同引用的模块就会被打包合并到名为children-async
的公共模块,当你懒加载first
或者second
的时候并行加载这和children-async
公共模块 -
children
为false
,共同引用的模块就会被打包到首屏加载的app.bundle
当中,这就会导致首屏加载过长了,而且也不要用到,所以最好还是设为true
浏览器缓存的实现
先来说一下哈希值的不同:
-
hash
是build-specific
,即每次编译都不同——适用于开发阶段 -
chunkhash
是chunk-specific
,是根据每个chunk
的内容计算出的hash
——适用于生产
所以,在生产环境,要把文件名改成'[name].[chunkhash]'
,最大限度的利用浏览器缓存。
最后,写这篇文章,自己测试了很多demo
,当然不可能全部贴上,但还是希望自己多动手测试以下,真的坑中带坑。
也参考了很多文章:
https://github.com/creeperyan...
https://segmentfault.com/q/10...
https://segmentfault.com/q/10...
https://www.jianshu.com/p/2b8...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。