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的主要特性,也是相對比較複雜的一個技術點,若是要應對複雜龐大的項目,就須要咱們對代碼分割的配置有更深的理解了。

相關文章
相關標籤/搜索