Webpack CommonsChunkPlugin插件研究

原由

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

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

搭建最简单实验项目

文件目录以下:webpack

image

src文件夹下放源代码,里面放了两个模块index.js和Greeter.js。dist文件夹下放输出文件。再看看webpack.config.js中的配置:web

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名:bash

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"便可。app

我在《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:async

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的主要特性,也是相对比较复杂的一个技术点,若是要应对复杂庞大的项目,就须要咱们对代码分割的配置有更深的理解了。

相关文章
相关标签/搜索