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
我們可以用樹結構描述上述配置的作用:
每一次使用CommonsChunkPlugin都會將共有程式碼和runtime提取到父節點。上述例子中,通過兩次CommonChunkPlugin的作用,runtime被提取到common.js中。通過這種樹型結構,我們可以清晰的看出每個頁面對各個chunk的依賴順序。
2.3 提取Runtime(執行時)程式碼
使用CommonsChunkPlugins時,一個常見的問題就是:
沒有被修改過的公有程式碼或庫程式碼打包出的Entry Chunk,會隨著其他業務程式碼的變化而變化,導致頁面上的長快取機制失效。
github上有一個與此相關的問題。本意就是在只修改業務程式碼時,而不改動庫程式碼時,打包出的庫程式碼的chunkhash也發生變化,導致瀏覽器端的長快取機制失效。如圖所示,app和vender的chunkhash都發生了變化。
這主要是因為使用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不會變,這樣才能最大化的利用瀏覽器的快取機制。如下圖所示:
修改業務程式碼後,vender的chunkhash不會變化,方便使用瀏覽器的快取:
由於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,
...
}
}
在業務程式碼的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很小。
需要注意的是: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充分地使用瀏覽器的快取。