上一小節介紹了如何做圖片加載相關的優化以及壓縮代碼,這一部分內容會稍微深入點,講解如何利用瀏覽器的緩存以及在 webpack 中實現按需加載代碼。
## 分離代碼文件
關于分離 CSS 文件這個主題,之前在介紹如何搭建基本的前端開發環境時有提及,在 webpack 中使用 [extract-text-webpack-plugin](https://doc.webpack-china.org/plugins/extract-text-webpack-plugin) 插件即可。
先簡單解釋一下為何要把 CSS 文件分離出來,而不是直接一起打包在 JS 中。最主要的原因是我們希望更好地利用緩存。
假設我們原本頁面的靜態資源都打包成一個 JS 文件,加載頁面時雖然只需要加載一個 JS 文件,但是我們的代碼一旦改變了,用戶訪問新的頁面時就需要重新加載一個新的 JS 文件。有些情況下,我們只是單獨修改了樣式,這樣也要重新加載整個應用的 JS 文件,相當不劃算。
還有一種情況是我們有多個頁面,它們都可以共用一部分樣式(這是很常見的,CSS Reset、基礎組件樣式等基本都是跨頁面通用),如果每個頁面都單獨打包一個 JS 文件,那么每次訪問頁面都會重復加載原本可以共享的那些 CSS 代碼。如果分離開來,第二個頁面就有了 CSS 文件的緩存,訪問速度自然會加快。雖然對第一個頁面來說多了一個請求,但是對隨后的頁面來說,緩存帶來的速度提升相對更加可觀。
因此當我們考慮更好地利用緩存來加速靜態資源訪問時,會嘗試把一些公共資源單獨分離開來,利用緩存加速,以避免重復的加載。除了公共的 CSS 文件或者圖片資源等,當我們的 JS 代碼文件過大的時候,也可以用代碼文件拆分的辦法來進行優化。
那么,如何使用 webpack 來把代碼中公共使用的部分分離成為獨立的文件呢?由于 webpack 4.x 和 webpack 3.x 在代碼分離這一塊的內容差別比較大,因而我們分別都介紹一下。
3.x 以前的版本是使用 CommonsChunkPlugin 來做代碼分離的,而 webpack 4.x 則是把相關的功能包到了 `optimize.splitChunks` 中,直接使用該配置就可以實現代碼分離。
我們先介紹在 webpack 4.x 中如何使用這個配置來實現代碼分離。
## webpack 4.x 的 optimization
webpack 的作者推薦直接這樣簡單地配置:
```
module.exports = {
// ... webpack 配置
optimization: {
splitChunks: {
chunks: "all", // 所有的 chunks 代碼公共的部分分離出來成為一個單獨的文件
},
},
}
```
我們需要在 HTML 中引用兩個構建出來的 JS 文件,并且 commons.js 需要在入口代碼之前。下面是個簡單的例子:
```
<script src="commons.js" charset="utf-8"></script>
<script src="entry.bundle.js" charset="utf-8"></script>
```
如果你使用了 html-webpack-plugin,那么對應需要的 JS 文件都會在 HTML 文件中正確引用,不用擔心。如果沒有使用,那么你需要從 `stats` 的 `entrypoints` 屬性來獲取入口應該引用哪些 JS 文件,可以參考 [Node API](https://doc.webpack-china.org/api/node/) 了解如何從 `stats` 中獲取信息,或者開發一個 plugin 來處理正確引用 JS 文件這個問題。第 15 小節會介紹如何開發 webpack plugin,plugin 提供的 API 也可以正確獲取到 `stats` 中的數據。
之前我們提到拆分文件是為了更好地利用緩存,分離公共類庫很大程度上是為了讓多頁面利用緩存,從而減少下載的代碼量,同時,也有代碼變更時可以利用緩存減少下載代碼量的好處。從這個角度出發,筆者建議將公共使用的第三方類庫顯式地配置為公共的部分,而不是 webpack 自己去判斷處理。因為公共的第三方類庫通常升級頻率相對低一些,這樣可以避免因公共 chunk 的頻繁變更而導致緩存失效。
顯式配置共享類庫可以這么操作:
```
module.exports = {
entry: {
vendor: ["react", "lodash", "angular", ...], // 指定公共使用的第三方類庫
},
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
test: "vendor",
name: "vendor", // 使用 vendor 入口作為公共部分
enforce: true,
},
},
},
},
// ... 其他配置
}
// 或者
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /react|angluar|lodash/, // 直接使用 test 來做路徑匹配
chunks: "initial",
name: "vendor",
enforce: true,
},
},
},
},
}
// 或者
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
chunks: "initial",
test: path.resolve(__dirname, "node_modules") // 路徑在 node_modules 目錄下的都作為公共部分
name: "vendor", // 使用 vendor 入口作為公共部分
enforce: true,
},
},
},
},
}
```
上述第一種做法是顯示指定哪些類庫作為公共部分,第二種做法實現的功能差不多,只是利用了 `test` 來做模塊路徑的匹配,第三種做法是把所有在 node\_modules 下的模塊,即作為依賴安裝的,都作為公共部分。你可以針對項目情況,選擇最合適的做法。
## webpack 3.x 的 CommonsChunkPlugin
下面我們簡單介紹一下在 webpack 3.x 中如何配置代碼分離。webpack 3.x 以下的版本需要用到 webpack 自身提供的 CommonsChunkPlugin 插件。我們先來看一個最簡單的例子:
```
module.exports = {
// ...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "commons", // 公共使用的 chunk 的名稱
filename: "commons.js", // 公共 chunk 的生成文件名
minChunks: 3, // 公共的部分必須被 3 個 chunk 共享
}),
],
}
```
chunk 在這里是構建的主干,可以簡單理解為一個入口對應一個 chunk。
以上插件配置在構建后會生成一個 commons.js 文件,該文件就是代碼中的公共部分。上面的配置中 `minChunks` 字段為 3,該字段的意思是當一個模塊被 3 個以上的 chunk 依賴時,這個模塊就會被劃分到 `commons` chunk 中去。單從這個配置的角度上講,這種方式并沒有 4.x 的 `chunks: "all"` 那么方便。
CommonsChunkPlugin 也是支持顯式配置共享類庫的:
```
module.exports = {
entry: {
vendor: ['react', 'react-redux'], // 指定公共使用的第三方類庫
app: './src/entry',
// ...
},
// ...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' // 使用 vendor 入口作為公共部分
filename: "vendor.js",
minChunks: Infinity, // 這個配置會讓 webpack 不再自動抽離公共模塊
}),
],
}
```
上述配置會生成一個名為 vendor.js 的共享代碼文件,里面包含了 React 和 React-Redux 庫的代碼,可以提供給多個不同的入口代碼使用。這里的 `minChunks` 字段的配置,我們使用了 `Infinity`,可以理解為 webpack 不自動抽離公共模塊。如果這里和之前一樣依舊設置為 3,那么被 3 個以上的 chunk 依賴的模塊會和 React、React-Redux 一同打包進 vendor,這樣就失去顯式指定的意義了。
`minChunks` 其實還可以是一個函數,如:
```
minChunks: (module, count) => {
console.log(module, count);
return true;
},
```
該函數在分析每一個依賴的時候會被調用,傳入當前依賴模塊的信息 `module`,以及已經被作為公共模塊的數量 `count`,你可以在函數中針對每一個模塊做更加精細化的控制。看一個簡單的例子:
```
minChunks: (module, count) => {
return module.context && module.context.includes("node_modules");
// node_modules 目錄下的模塊都作為公共部分,效果就如同 webpack 4.x 中的 test: path.resolve(__dirname, "node_modules")
},
```
更多使用 CommonsChunkPlugin 的配置參考官方文檔 [commons-chunk-plugin](https://doc.webpack-china.org/plugins/commons-chunk-plugin/#-)。
而關于 webpack 4.x 的 splitChunks 配置,筆者寫這一部分的時候官方文檔還沒有更新出來,上述配置預估可以滿足大部分項目的需求,更加詳細的內容還請等待官方文檔更新后查閱。
## 小結
本小節是優化前端資源加載這個主題的第二部分,主要分別介紹了在 webpack 4.x 版本和 3.x 版本中,如何配置分離代碼文件來更加高效地利用瀏覽器緩存。webpack 兩個版本關于分離代碼這一塊的使用差異比較大,筆者還是推薦使用 4.x 版本,因為它的配置相對來說要更加簡單一些。接下來第 11 小節會介紹優化前端資源加載的最后一個部分的內容。
## 例子
本小節提及的一些簡單的 Demo 可以在 [webpack-examples](https://github.com/teabyii/webpack-examples) 找到。