WebPack持久快取學習小結

雙皮奶發表於2019-03-04

持久快取

使用webpack構建工程的時候,我們往常會把功能不同的程式碼打包到不同的包裡(如lib,vendor,業務程式碼)。 而持久快取的目的就是每一次更新線上程式碼的時候,儘可能使內容未做更改的模組的名字和之前保持一致。

使用webpack實現持久快取主要需要解決:

  1. webpack runtime程式碼分離

  2. 穩定moduleID

  3. 穩定chunkID

基本配置
webpack有提供2種hash命名的方式

  1. [hash]: 整次build生成一個唯一的hash值,賦給所有生成的檔案。

顯然,為了將檔名和內容相關聯,應該使用chunkhash。

  output: {
    path: path.join(__dirname, `dist/js`),
    filename: `[name]-[chunkhash:8].js`,
    chunkFilename: `chunks/[name].[chunkhash:8].js`
  },
複製程式碼
build結果
build結果

這樣配置雖然可以實現持久快取,但是把所有的程式碼都打到了一起(vendor, 業務程式碼等),顯示不符合要求。

webpack-runtime

使用CommonsChunkplugin將vendor單獨打包:

new webpack.optimize.CommonsChunkPlugin({
    name: `vendor`,
    minChunks: function(module) {
      return /node_modules/.test(module.context);
    }
})
複製程式碼
打包結果
第三方庫被打到vendor裡

現在如果我對業務程式碼進行更改:

const helloWorld = () => {
  console.log(`moment : ${moment()}`);
};
複製程式碼

按道理vendor已經單獨打包,改變業務程式碼並不應該改變vendor的hash值,然而

打包結果
vendor的hash值變了

原因是CommonsChunkplugin把vendor單獨打出來的時候,還會將webpack自己生成的一部分runtime程式碼一起打進vendor. 如下圖,runtime裡牽扯到chunkid等容易頻繁變更的元素,所以當業務程式碼發生變化的時候,runtime程式碼也會變。

這個問題也容易解決,再用CommonsChunkplugin把runtime程式碼單獨打出來(CommonsChunkplugin會把runtime的程式碼打到配置指定的最後一個chunk裡):

new webpack.optimize.CommonsChunkPlugin({
    name: `manifest`,
    minChunks: Infinity
})
複製程式碼
打包結果
runtime程式碼被單獨提取到manifest裡

.
現在再更改業務程式碼,業務程式碼和manifest的內容會變,vendor檔案的內容不會受影響。

Module ID

然而還沒完,當我們在業務程式碼裡增加一個entry,vendor的hash值又發生了變化。

entry: {
    main: "./src/index.tsx",
    sub: "./src/sub.tsx",
},
複製程式碼
WebPack持久快取學習小結

造成這個問題的原因是當我們加入一個新entry的時候,因為webpack預設會依次用整數給這些module命名,如果新增的entry有新引入其他module,就會打亂之前module的命名。比如說當只有一個entry的時候,業務程式碼裡定義了module: 0,1,2,3。vendor裡定義了module 4,5。增加一個entry後:業務程式碼裡會定義: 0,1,2,3,4。 vendor裡定義module 5,6.
(其實,就算sub沒有引入新module,這裡如果把main和sub的位置換一下也會造成vendor hash值變化,這和chunkID的值有關,後面會介紹)

可見,要生成穩定的chunkhash值,首先必須解決moduleID的問題。

NamedModulesPlugin & HashedModuleIdsPlugin

NamedModulesPlugin:使用檔案的相對路徑代替整數作為moduleID.

var isNumber = __webpack_require__("./node_modules/array-first/node_modules/is-number/index.js");
var slice = __webpack_require__("./node_modules/array-slice/index.js");
複製程式碼

不過也帶來2個問題:

1.用相對路徑代替數字,檔案變大了

2.相對路徑暴露了

HashedModuleIdsPlugin
:主要就是為了解決以上2個問題,它對相對路徑進行一個md5的摘要,不僅避免檔案過大,也隱藏了路徑。

Chunk ID

有時候一些模組可能在頁面初始化的時候並用不到,可能會在之後的過程中(比如使用者點選事件)才會用到,這一類模組可以通過動態import()來引入。

const helloWorld = () => {
 import(`./sub`).then((module) => {
     const sub = module.default;
     sub();
   })
};
複製程式碼
`打包結果`
動態引入的chunk竟然是用數字命名的

可以看到,動態引入的程式碼會被單獨打包到chunks/裡的檔案裡,而且vendor的值再次發生了改變。造成這個的原因是和moduleID類似,webpack預設使用整數作為chunkID,並且非同步載入的chunk會先被賦值。 也就是說在沒有動態引入之前,vendor的chunkID是0,動態引入之後,vendor檔案的chunkID變為了1,所以造成了內容變化。(上面提到的改變entry順序也會造成chunkID的變化)

NamedChunksPlugin:這個外掛會用chunk的字串name代替整數作為chunkID。不過要注意的是,動態生成的chunk並沒有名字,所以需要手動給取個名字。

new webpack.NamedChunksPlugin(function(chunk) {
    if (chunk.name) {
        return chunk.name;
    }
    return chunk.mapModules(function(m){ return m.id}).join("_");
})
複製程式碼
打包結果
用該chunk所用到的所有module的ID來命名chunk

至此,可能造成hash值不穩定的元素都被幹掉。

Webpack4 更新

webpack4更新的內容

  1. CommonsChunksPlugin被幹掉了,解決runtime貌似只需要使用optimization.runtimeChunk就能搞定了
  2. NamedModulesPlugin -> optimization.namedModules (on by default in develoment mode) 開發模式下預設會使用NamedModulesPlugin。

目測我們還是需要自己解決chunkID的問題,具體怎麼操作等下個月出了穩定版再學習吧。

參考文獻

Predictable long term caching with Webpack

webpack文件

用 webpack 實現持久化快取

相關文章