詳解CommonsChunkPlugin的配置和用法

簡介

CommonsChunkPlugin主要是用來提取第三方庫和公共模塊,避免首屏加載的bundle文件或者按需加載的bundle文件體積過大,從而致使加載時間過長,着實是優化的一把利器。javascript

先來講一下各類教程以及文檔中CommonsChunkPlugin說起到chunk有哪幾種,主要有如下三種:java

  1. webpack當中配置的入口文件(entry)chunk,能夠理解爲entry chunk
  2. 入口文件以及它的依賴文件經過code split(代碼分割)出來的也是chunk,能夠理解爲children chunk
  3. 經過CommonsChunkPlugin建立出來的文件也是chunk,能夠理解爲commons chunk

CommonsChunkPlugin可配置的屬性:

  • name:能夠是已經存在的chunk(通常指入口文件)對應的name,那麼就會把公共模塊代碼合併到這個chunk上;不然,會建立名字爲namecommons chunk進行合併

* filename:指定commons chunk的文件名
* chunks:指定source chunk,即指定從哪些chunk當中去找公共模塊,省略該選項的時候,默認就是entry chunks
* minChunks:既能夠是數字,也能夠是函數,還能夠是Infinity,具體用法和區別下面會說node

childrenasync屬於異步中的應用,放在了最後講解。jquery

可能這麼說,你們會雲裏霧裏,下面用demo來檢驗上面的屬性。webpack

實戰應用

如下幾個demo主要是測試如下幾種狀況:git

  • 不分離出第三方庫和自定義公共模塊
  • 分離出第三方庫、自定義公共模塊、webpack運行文件,但它們在同一個文件中
  • 單獨分離第三方庫、自定義公共模塊、webpack運行文件,各自在不一樣文件

不分離出第三方庫和自定義公共模塊

項目初始結構,後面打包後會生成dist目錄:
imagegithub

src目錄下各個文件內容都很簡潔的,以下:web

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文件:npm

{
  "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:json

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目錄:

image

再來查看一下命令行中webpack的打包信息:

image

查看first.jssecond.js,會發現共同引用的common.js文件和jquery都被打包進去了,這確定不合理,公共模塊重複打包,體積過大。

分離出第三方庫、自定義公共模塊、webpack運行文件

這時候修改webpack.config.js新增一個入口文件vendorCommonsChunkPlugin插件進行公共模塊的提取:

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的文件:

image

再來查看一下命令行中webpack的打包信息:

image

經過查看vendor.js文件,發現first.jssecond.js文件中依賴的jquerycommon.js都被打包進vendor.js中,同時還有webpack的運行文件。總的來講,咱們初步的目的達到,提取公共模塊,可是它們都在同一個文件中。

到這裏,確定有人但願自家的vendor.js純白無瑕,只包含第三方庫,不包含自定義的公共模塊和webpack運行文件,又或者但願包含第三方庫和公共模塊,不包含webpack運行文件。

其實,這種想法是對,特別是分離出webpack運行文件,由於每次打包webpack運行文件都會變,若是你不分離出webpack運行文件,每次打包生成vendor.js對應的哈希值都會變化,致使vendor.js改變,但實際上你的第三方庫實際上是沒有變,然而瀏覽器會認爲你原來緩存的vendor.js就失效,要從新去服務器中獲取,其實只是webpack運行文件變化而已,就要人家從新加載,好冤啊~

OK,接下來就針對這種狀況來測試。

單獨分離出第三方庫、自定義公共模塊、webpack運行文件

這裏咱們分兩步走:

先單獨抽離出webpack運行文件
接着單獨抽離第三方庫和自定義公共模塊,這裏利用minChunks有兩種方法能夠完成,日後看就知道了

一、抽離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運行文件代碼的意思是建立一個名爲runtimecommons chunk進行webpack運行文件的抽離,其中source chunksvendor.js

查看dist目錄下,新增了一個runtime.js的文件,其實就是webpack的運行文件:

image

再來查看一下命令行中webpack的打包信息,你會發現vendor.js的體積已經減少,說明已經把webpack運行文件提取出來了:

image

但是,vendor.js中還有自定義的公共模塊common.js,人家只想vendor.js擁有項目依賴的第三方庫而已(這裏是jquery),這個時候把minChunks這個屬性引進來。

minChunks能夠設置爲數字、函數和Infinity,默認值是2,並非官方文檔說的入口文件的數量,下面解釋下minChunks含義:

  • 數字:模塊被多少個chunk公共引用才被抽取出來成爲commons chunk
  • 函數:接受 (module, count) 兩個參數,返回一個布爾值,你能夠在函數內進行你規定好的邏輯來決定某個模塊是否提取成爲commons chunk
  • Infinity:只有當入口文件(entry chunks) >= 3 才生效,用來在第三方庫中分離自定義的公共模塊
二、抽離第三方庫和自定義公共模塊

要在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的文件:

image

再來查看一下命令行中webpack的打包信息,自定義的公共模塊分離出來:

image

這時候的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']
        }),
    ]
}

上面的代碼其實就是生成一個叫作vendorcommons chunk,那麼有哪些模塊會被加入到vendor中呢?就對入口文件及其依賴的模塊進行遍歷,若是該模塊是js文件而且在node_modules中,就會加入到vendor當中,其實這也是一種讓vendor只保留第三方庫的辦法。

再來查看一下命令行中webpack的打包信息:

image

你會發現,和上面minChunks設爲Infinity的結果是一致的。

children和async屬性

這兩個屬性主要是在code split(代碼分割)和異步加載當中應用。

  • children

    • 指定爲true的時候,就表明source chunks是經過entry chunks(入口文件)進行code split出來的children chunks
    • childrenchunks不能同時設置,由於它們都是指定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

修改爲異步截圖出來太麻煩了,就簡單說明一下:firstsecond是異步加載模塊,同時它們共同引用了common.js這個模塊,若是你不設置這一步:

new webpack.optimize.CommonsChunkPlugin({
        children: true,
        async: 'children-async'
    })

那麼共同引用的common.js都被打包進各自的模塊當中,就重複打包了。

OK,你設置以後,也得看children的臉色怎麼來劃分:

  • childrentrue,共同引用的模塊就會被打包合併到名爲children-async的公共模塊,當你懶加載first或者second的時候並行加載這和children-async公共模塊
  • childrenfalse,共同引用的模塊就會被打包到首屏加載的app.bundle當中,這就會致使首屏加載過長了,並且也不要用到,因此最好仍是設爲true

瀏覽器緩存的實現

先來講一下哈希值的不一樣:

  • hashbuild-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...

相關文章
相關標籤/搜索