1. 程式人生 > >webpack程式碼分割技巧

webpack程式碼分割技巧

1. 程式碼中定義分割點

webpack支援在程式碼中定義分割點。分割點指定的模組只有在真正使用時才載入,可以使用webpack提供的require.ensure語法:

$('#okButton').click(function(){
  require.ensure(['./foo'], function(require) {
    var foo = require('./foo');
    //your code here
  });
});

也可以像RequireJS一樣使用AMD語法:

$('#okButton').click(function(){
  require(['foo'],function(foo){
    // your code here
  }]);
});

上面兩種方式都會以foo模組為入口將其依賴模組遞迴地打包到一個新的Chunk,並在#okButton按鈕點選時才非同步地載入這個以foo模組為入口的新的chunk。

2. 使用CommonsChunkPlugin分割程式碼

在理解CommonsChunkPlugin程式碼分割之前,我們需要熟悉webpack中chunk的概念,webpack將多個模組打包之後的程式碼集合稱為chunk。根據不同webpack配置,chunk又有如下幾種型別:

Entry Chunk: 包含一系列模組程式碼,以及webpack的執行時(Runtime)程式碼,一個頁面只能有一個Entry Chunk,並且需要先於Normal Chunk載入

Normal Chunk: 只包含一系列模組程式碼,不包含執行時(Runtime)程式碼。

作為webpack程式碼分割的利器,網路上有太多CommonsChunkPlugin的文章,但以某一使用場景的入門案例為主。本文我們根據不同場景下的使用方法,分別介紹。

2.1 提取庫程式碼

假設我們需要將很少變化的常用庫(react、lodash、redux)等與業務程式碼分割,可以在webpack.config.js採用如下配置:

var webpack = require("webpack");
module.exports = {
  entry: {
    app: "./app.js",
    vendor: ["lodash","jquery"],
  },
  output: {
    path: "release",
    filename: "[name].[chunkhash].js"
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({names: ["vendor"]})
  ]
};

上述配置將常用庫打包到一個vender命名的Entry Chunk,並將以app.js為入口的業務程式碼打包到一個以business命名的Normal Chunk。其中Entry Chunk包含了webpack的執行時(Runtime)程式碼,所以在頁面中必須先於業務程式碼載入。

2.2 提取公有程式碼

假設我們有多個頁面,為了優化網路載入效能,我們需要將多個頁面共用的程式碼提取出來單獨打包。可以在webpack.config.js進行如下配置:

var webpack = require("webpack");
module.exports = {
    entry: { 
          page1: "./page1.js", 
          page2: "./page2.js" 
        },
    output: { 
          filename: "[name].[chunkhash].js" 
        },
    plugins: [ new webpack.optimize.CommonsChunkPlugin("common.[chunkhash].js") ]
}

上述配置將兩個頁面中通用的程式碼抽取出來並打包到以common命名的Entry Chunk,並將以page1.js和page2.js為入口程式碼分別打包到以page1和page2命名的Normal Chunk。 其中Entry Chunk包含了webpack的執行時(Runtime)程式碼,所以common.[chunkhash].js在兩個頁面中都必須在page1.[chunkhash].js和page2.[chunkhash].js前載入。

在這種配置下,CommonsChunkPlugin的作用可以抽象:

將多個入口中的公有程式碼和Runtime(執行時)抽取到父節點

理解了CommonsChunkPlugin的本質後,我們看一個更復雜的例子:

var webpack = require("webpack");
module.exports = {
    entry: {
        p1: "./page1",
        p2: "./page2",
        p3: "./page3",
        ap1: "./admin/page1",
        ap2: "./admin/page2"
    },
    output: {
        filename: "[name].js"
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
        new webpack.optimize.CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
    ]
};
// page1.html: commons.js, p1.js
// page2.html: commons.js, p2.js
// page3.html: p3.js
// admin-page1.html: commons.js, admin-commons.js, ap1.js
// admin-page2.html: commons.js, admin-commons.js, ap2.js

我們可以用樹結構描述上述配置的作用:

commonchunk

每一次使用CommonsChunkPlugin都會將共有程式碼和runtime提取到父節點。上述例子中,通過兩次CommonChunkPlugin的作用,runtime被提取到common.js中。通過這種樹型結構,我們可以清晰的看出每個頁面對各個chunk的依賴順序。

2.3 提取Runtime(執行時)程式碼

使用CommonsChunkPlugins時,一個常見的問題就是:

沒有被修改過的公有程式碼或庫程式碼打包出的Entry Chunk,會隨著其他業務程式碼的變化而變化,導致頁面上的長快取機制失效。

github上有一個與此相關的問題。本意就是在只修改業務程式碼時,而不改動庫程式碼時,打包出的庫程式碼的chunkhash也發生變化,導致瀏覽器端的長快取機制失效。如圖所示,app和vender的chunkhash都發生了變化。

commonchunk

commonchunk

這主要是因為使用CommonsChunkPlugin提取程式碼到新的chunk時,會將webpack執行時(Runtime)也提取到打包後的新的chunk。通過如下配置就可以將webpack的runtime單獨提取出來:

var webpack = require("webpack");
module.exports = {
  entry: {
    app: "./app.js",
    vendor: ["lodash","jquery"],
  },
  output: {
    path: 'release',
    filename: "[name].[chunkhash].js"
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({names: ['vendor','runtime']}),
  ]
};

這種情況下,當業務程式碼傳送變化,而庫程式碼沒有改動時,vender的chunkhash不會變,這樣才能最大化的利用瀏覽器的快取機制。如下圖所示:

commonchunk

修改業務程式碼後,vender的chunkhash不會變化,方便使用瀏覽器的快取:

commonchunk

由於webpack的runtime比較小,我們可以直接將該檔案的內容inline到html中。

3. 使用DllPlugin和DllReferencePlugin分割程式碼

通過DllPlugin和DllReferencePlugin,webpack引入了另外一種程式碼分割的方案。我們可以將常用的庫檔案打包到dll包中,然後在webpack配置中引用。業務程式碼的可以像往常一樣使用require引入依賴模組,比如require('react'), webpack打包業務程式碼時會首先查詢該模組是否已經包含在dll中了,只有dll中沒有該模組時,webpack才將其打包到業務chunk中。

首先我們使用DllPlugin將常用的庫打包在一起:

var webpack = require('webpack');
module.exports = {
  entry: {
    vendor: ['lodash','react'],
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: 'build/',
  },
  plugins: [new webpack.DllPlugin({
    name: '[name]_lib',
    path: './[name]-manifest.json',
  })]
};

該配置會產生兩個檔案,模組庫檔案:vender.[chunkhash].js和模組對映檔案:vender-menifest.json。其中vender-menifest.json標明瞭模組路徑和模組ID(由webpack產生)的對映關係,其檔案內容如下:

{
  "name": "vendor_lib",
  "content": {
    "./node_modules/.npminstall/lodash/4.17.2/lodash/lodash.js": 1,
    "./node_modules/.npminstall/webpack/1.13.3/webpack/buildin/module.js": 2,
    "./node_modules/.npminstall/react/15.3.2/react/react.js": 3,
    ...
    }
}    

commonchunk

在業務程式碼的webpack配置檔案中使用DllReferencePlugin外掛引用模組對映檔案:vender-menifest.json後,我們可以正常的通過require引入依賴的模組,如果在vender-menifest.json中找到依賴模組的路徑對映資訊,webpack會直接使用dll包中的該依賴模組,否則將該依賴模組打包到業務chunk中。

var webpack = require('webpack');
module.exports = {
  entry: {
    app: ['./app'],
  },
  output: {
    filename: '[name].[chunkhash].js',
    path: 'build/',
  },
  plugins: [new webpack.DllReferencePlugin({
    context: '.',
    manifest: require('./vendor-manifest.json'),
  })]
};

由於依賴的模組都在dll包中,所以例子中app打包後的chunk很小。

commonchunk

需要注意的是:dll包的程式碼是不會執行的,需要在業務程式碼中通過require顯示引入。相比於CommonChunkPlugin,使用DllReferencePlugin分割程式碼有兩個明顯的好處:

(1)由於dll包和業務chunk包是分開進行打包的,每一次修改程式碼時只需要對業務chunk重新打包,webpack的編譯速度得到極大的提升,因此相比於CommonChunkPlugin,DllPlugin進行程式碼分割可以顯著的提升開發效率。

(2)使用DllPlugin進行程式碼分割,dll包和業務chunk相互獨立,其chunkhash互不影響,dll包很少變動,因此可以更充分的利用瀏覽器的快取系統。而使用CommonChunk打包出的程式碼,由於公有chunk中包含了webpack的runtime(執行時),公有chunk和業務chunk的chunkhash會互相影響,必須將runtime單獨提取出來,才能對公有chunk充分地使用瀏覽器的快取。