前言
如何基於webpack做持久化快取目前感覺是一直沒有一個非常好的方案來實踐。網上的文章非常多,但是真的有用的非常少,並沒有一些真正深入研究和總結的文章。現在依託于于早教寶線上專案和自己的實踐,有了一個完整的方案。
正文
1、webpack的hash的兩種計算方式
想要做持久化快取那麼就要依賴 webpack
自身提供的兩個 hash
:hash
和chunkhash
。
接著就來看看這兩個值之間的具體含義和差別吧:
hash
: webpack
在每一次構建的時候都會產生一個compilation
物件,這個hash
值就是根據compilation
內所有的內容計算而來的值。
chunkhash
:這個值是根據每個chunk
的內容而計算出來的值。
所以單純根據上面的描述來說,chunkhash
是用來做持久化快取最有效的。
2、hash和chunkhash的測試
entry | 入口檔案 | 入口依賴 |
---|---|---|
pageA | a.js | a.less->a.css, common.js->common.css |
pageB | b.js | b.less->b.css, common.js->common.css |
- 使用hash計算
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
entry: {
pageA: './src/a.js',
pageB: './src/b.js'
},
output: {
filename: '[name]-[hash].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?minimize']
})
}
]
},
plugins: [new ExtractTextPlugin('[name]-[hash].css')]
}複製程式碼
構建結果
Hash: 80c922b349f516e79fb5
Version: webpack 3.8.1
Time: 1014ms
Asset Size Chunks Chunk Names
pageB-80c922b349f516e79fb5.js 2.86 kB 0 [emitted] pageB
pageA-80c922b349f516e79fb5.js 2.84 kB 1 [emitted] pageA
pageA-80c922b349f516e79fb5.css 21 bytes 1 [emitted] pageA
pageB-80c922b349f516e79fb5.css 21 bytes 0 [emitted] pageB複製程式碼
結論
可以發現所有檔案的hash
全部都是一樣的,但是你多構建幾次產生的hash
都是不一樣的。原因在於我們使用了 ExtractTextPlugin
,ExtractTextPlugin
本身涉及到非同步的抽取流程,所以在生成 assets
資源時存在了不確定性(先後順序),而 updateHash
則對其敏感,所以就出現瞭如上所說的 hash 異動的情況。另外所有 assets
資源的 hash
值保持一致,這對於所有資源的持久化快取來說並沒有深遠的意義。
- 使用chunkhash計算
```js
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
entry: {
pageA: './src/a.js',
pageB: './src/b.js'
},
output: {
filename: '[name]-[chunkhash].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
]{ test: /\.css$/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: ['css-loader?minimize'] }) }複製程式碼
},
plugins: [new ExtractTextPlugin('[name]-[chunkhash].css')]
}
**構建結果**
```js
Hash: 810904f973cc0cf41992
Version: webpack 3.8.1
Time: 1038ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-3a2e5ef3d4506fce8d93.css 21 bytes 1 [emitted] pageA
pageB-e9ed5150262ba39827d4.css 21 bytes 0 [emitted] pageB複製程式碼
結論
此時可以發現,執行多少次,hash 的變動沒有了,每個 entry 擁有了自己獨一的 hash 值,細心的你或許會發現此時樣式資源的 hash 值和 入口指令碼保持了一致,這似乎並不符合我們的想法,冥冥之中告訴我們發生了某些壞事情。
3、探索css檔案的hash和入口檔案hash之間的關係
在上面的構建結果中,我們發現css的hash值和入口檔案的hash值是一樣的,這裡我們容易產生疑問,是不是這兩個檔案之間一定會有聯絡呢?呆著疑問去修改下b.css檔案中的內容,產生構建結果:
Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1028ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-3a2e5ef3d4506fce8d93.css 21 bytes 1 [emitted] pageA
pageB-e9ed5150262ba39827d4.css 41 bytes 0 [emitted] pageB複製程式碼
納尼???改動css檔案內容,為什麼css檔案的hash沒有改變呢?不科學啊,入口檔案的hash也沒有改變。仔細想了一下 webpack 是將所有的內容都認為是js檔案的一部分。在構建的過程中使用 ExtractTextPlugin 將樣式抽離出entry chunk 了,而此時的 entry chunk 本身並沒有發生改變,改變的是已經被抽離出去的css部分。而chunkunhash 卻是根據 chunk 計算出來的,所以不變更應該是正常的。但是這個又不符合我們想要做的持久化快取的要求,因為又變動就應該改變hash才是。
開心的是 ExtractTextPlugin 外掛為我們提供了一個contenthash
來變化:
plugins: [new ExtractTextPlugin('[name]-[contenthash].css')]複製程式碼
修改b.css前後兩次構建結果:
Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1091ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-9783744431577cdcfea658734b7db20f.css 21 bytes 1 [emitted] pageA
pageB-2d03aa12ae45c64dedd7f66bb88dd3db.css 41 bytes 0 [emitted] pageB複製程式碼
Hash: 7a96bcf1ef668a49c9d8
Version: webpack 3.8.1
Time: 1193ms
Asset Size Chunks Chunk Names
pageB-e9ed5150262ba39827d4.js 2.86 kB 0 [emitted] pageB
pageA-3a2e5ef3d4506fce8d93.js 2.84 kB 1 [emitted] pageA
pageA-9783744431577cdcfea658734b7db20f.css 21 bytes 1 [emitted] pageA
pageB-7e05e00e24f795b674df5701f6a38bd9.css 42 bytes 0 [emitted] pageB複製程式碼
對比發現修改了樣式檔案後只有樣式檔案的hash發生了改變,符合我們想要的預期。
4、module id的不可控和修正
經過上面的測試,我們理所當然的認為我完成了持久化快取的hash穩定。然後我們不小心刪除了a.js中的a.less檔案,然後前後兩次構建:
Hash: 88ab71080c53db9d9f70
Version: webpack 3.8.1
Time: 1279ms
Asset Size Chunks Chunk Names
pageB-a2d1e1d73336f17e2dc4.js 3.82 kB 0 [emitted] pageB
pageA-96c9f5afea30e7e09628.js 3.8 kB 1 [emitted] pageA
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 1 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 0 [emitted] pageB複製程式碼
Hash: 172153ea2b39c2046a92
Version: webpack 3.8.1
Time: 1260ms
Asset Size Chunks Chunk Names
pageB-884da67fe2322246ab28.js 3.81 kB 0 [emitted] pageB
pageA-4c0dfb634722c556ffa0.js 3.68 kB 1 [emitted] pageA
pageA-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 1 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 0 [emitted] pageB複製程式碼
奇怪的事產生了,我移除了a.less檔案後發現pageB入口檔案的hash都改變了。如果只有pageA相關的檔案hash變了我還可以理解。但是????為什麼都變了???不行我得看看為什麼都變了。
通過上面的diff發現我們移除了a.less後整體的id發生了改變了。那麼這個地方的id我們可以推測是代表的是具體的引用的模組。
接著我們在看看前後兩次構建模組的資訊:
[3] ./src/a.js 284 bytes {1} [built]
[4] ./src/a.less 41 bytes {1} [built]
[5] ./src/b.js 284 bytes {0} [built]
[6] ./src/b.less 41 bytes {0} [built]複製程式碼
[3] ./src/a.js 264 bytes {1} [built]
[4] ./src/b.js 284 bytes {0} [built]
[5] ./src/b.less 41 bytes {0} [built]複製程式碼
通過對比發現前面的序號在構建出來的pageB中有隱藏pageA相關的資訊,這對於我們來做持久化快取來說是非常不便的。我們期待的是pageB中只包含和自身相關的資訊,不包含其他與自身無關的資訊。
5、module id的變化
排除與己不相關的module id或者內容
會用webpack的人大概都之都一個特性:Code Splitting
,本質上是對 chunk 進行拆分再組合的過程。具體要怎麼做呢?
The answer is CommonsChunkPlugin
,在plugin中新增:
plugins: [
new ExtractTextPlugin('[name]-[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
]複製程式碼
接下來在看看移除pageA中的a.less的前後變化:
Hash: 697b36118920d991364a
Version: webpack 3.8.1
Time: 1488ms
Asset Size Chunks Chunk Names
pageB-9b2eb6768499c911a728.js 491 bytes 0 [emitted] pageB
pageA-c342383ca09604e8e7b8.js 495 bytes 1 [emitted] pageA
runtime-b6ec3c0d350aef6cbf3e.js 6.8 kB 2 [emitted] runtime
pageA-b812cf5b72744af29181f642fe4dbf38.css 43 bytes 1 [emitted] pageA
pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css 59 bytes 0 [emitted] pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] runtime複製程式碼
Hash: 7ddaf109d5aa67c43ce2
Version: webpack 3.8.1
Time: 1793ms
Asset Size Chunks Chunk Names
pageB-613cc5a6a90adfb635f4.js 491 bytes 0 [emitted] pageB
pageA-0b72f85fda69a9442076.js 375 bytes 1 [emitted] pageA
runtime-a41b8b8bfe7ec70fd058.js 6.79 kB 2 [emitted] runtime
pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css 59 bytes 0 [emitted] pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] runtime複製程式碼
接著在看看兩次構建中pageB的對比:
經過對比我們發現在pageB中只包含的是自身相關的內容。所以使用CommonsChunkPlugin達到了我們的期望。而抽離出去的程式碼就是webpack的執行時程式碼。執行時程式碼也儲存著webpack對module和chunk相關的資訊。另外我們發現pageA和pageB的檔案大小也發生了變化。導致這個變化的原因是CommonsChunkPlugin會預設的把entry chunk都包含的module抽取到我們取名為runtime的normal chunk中去。
假如我們在開發中每個頁面都會用到一些工具庫,例如lodash這類的。由於CommonsChunkPlugin的預設行為會抽取公共部分,可能lodash並沒有發生改變,但是被抽離在執行時程式碼中的時候,每次都是會去請求新的。這不能達到我們要求的最小更新原則。所以我們要人工去幹預一些程式碼。
plugins: [
new ExtractTextPlugin('[name]-[contenthash].css'),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})複製程式碼
在次對邊前後兩次構建的日誌:
Hash: a703a57c828ec32b24e1
Version: webpack 3.8.1
Time: 1493ms
Asset Size Chunks Chunk Names
vendor-f11f58b8150930590a10.js 541 kB 0 [emitted] [big] vendor
pageB-7d065cd319176f44c605.js 938 bytes 1 [emitted] pageB
pageA-2b7e3707314e7ec4d770.js 910 bytes 2 [emitted] pageA
runtime-e68dec8bcad8a5870f0c.js 5.88 kB 3 [emitted] runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB複製程式碼
Hash: 26fc9ad18554b28cd8e1
Version: webpack 3.8.1
Time: 1806ms
Asset Size Chunks Chunk Names
vendor-d9bad56677b04b803651.js 541 kB 0 [emitted] [big] vendor
pageB-a55dadfbf25a45856d6a.js 929 bytes 1 [emitted] pageB
pageA-7cbd77a502262ddcdd19.js 790 bytes 2 [emitted] pageA
runtime-fa8eba6e81ed41f50d6f.js 5.88 kB 3 [emitted] runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB複製程式碼
到此為止我們解決了:排除與己不相關的module id或者內容問題。
穩定module id,儘可能的保持module id保持不變
一個module id是一個模組的唯一標示,並且該標示會出現在對應的entry chunk構建後的程式碼中。看個pageB的構建後程式碼的例子:
__webpack_require__(7)
const sum = __webpack_require__(0)
const _ = __webpack_require__(3)複製程式碼
根據前面的實驗,模組的增加或者減少都會引起module id的改變,所以為了不引起module id的改變,那麼我們只能找一個東西來代替module id作為標示。我們在構建的過程中就將尋找出來替代標示來替換module id。
所以上面的敘述可以轉換成兩個步驟來行動。
- 找到替代module id的方式
- 找到時機替換module id
6、穩定 module id 的相關操作
找到替代module id的方式
我們在日常的開發中,經常引用模組,都是通過地址來引用的。從這裡我們可以得到啟發,我們能不能夠把module id全部替換成路徑呢?再一個我們瞭解到在webpack resolve module階段我們肯定是可以拿到資源路徑的。在開始我們擔心平臺的路徑差異性。幸運的是webpack 的原始碼其中在 ContextModule#74 和 ContextModule#35 中 webpack 對 module 的路徑做了差異性修復。也就是說我們可以放心的通過module的libIdent來獲取模組的路徑了。
在整個webpack的執行過程中涉及到module id有三個鉤子:
before-module-ids
-> optimize-module-ids
-> after-optimize-module-ids
所以我們只要在before-module-ids
中做出修改就好了。
編寫外掛:
'use strict'
class moduleIDsByFilePath {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin("before-module-ids", (modules) => {
modules.forEach((module) => {
if(module.id === null && module.libIdent) {
module.id = module.libIdent({
context: this.options.context || compiler.options.context
})
}
})
})
})
}
}
module.exports = moduleIDsByFilePath複製程式碼
上面的其實已經被webpack抽成一個外掛了:
NamedModulesPlugin複製程式碼
所以只需要在外掛那一部分裡面新增上
new webpack.NamedModulesPlugin()複製程式碼
接下來對比下兩次構建前後檔案的變化:
Hash: e5bc78237ca9a3ad31f8
Version: webpack 3.8.1
Time: 1508ms
Asset Size Chunks Chunk Names
vendor-ebd9bfc583f45a344630.js 541 kB 0 [emitted] [big] vendor
pageB-432105effc229524c683.js 1.09 kB 1 [emitted] pageB
pageA-158bf2a923c98ab49be2.js 1.09 kB 2 [emitted] pageA
runtime-9ca4cebe90e444e723b9.js 5.88 kB 3 [emitted] runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css 92 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB複製程式碼
Hash: 7dce5d9dc88f619522fe
Version: webpack 3.8.1
Time: 1422ms
Asset Size Chunks Chunk Names
vendor-ebd9bfc583f45a344630.js 541 kB 0 [emitted] [big] vendor
pageB-432105effc229524c683.js 1.09 kB 1 [emitted] pageB
pageA-dae883ddaeff861761da.js 940 bytes 2 [emitted] pageA
runtime-c874a0c304fa03493296.js 5.88 kB 3 [emitted] runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css 49 bytes 2 [emitted] pageA
pageB-56185455ea60f01155a65497e9bf6c85.css 108 bytes 1 [emitted] pageB複製程式碼
哇,我們對比發現只有相關改動的檔案和執行時程式碼發生了改變,vendor和pageB相關都沒有發生改變。美滋滋~~
這下我們達到了我們的目的,我們可以去看看我們構建後的程式碼了:
__webpack_require__("./src/b.less")
const sum = __webpack_require__("./src/common.js")
const _ = __webpack_require__("./node_modules/lodash/lodash.js")複製程式碼
真的是變成了路徑,成功~~。但是新的問題貌似又來了,和之前的檔案對比發現我們的檔案普遍比之前的變大了。好吧,是我們換成檔案路徑的時候造成的。這個時候我們能不能用hash來代替檔案路徑呢?答案是可以,官方也有外掛可以供我們使用:
new webpack.HashedModuleIdsPlugin()複製程式碼
官方說 NamedModulesPlugin 適合在開發環境,而在生產環境下請使用 HashedModuleIdsPlugin。
這樣我們就達成了使用hash來代替原來的module id使之穩定。而且構建後的程式碼也不會變化太大。
本以為可以到此為止了。但是細心的人會發現runtime檔案每次編譯都發生了變化。是什麼導致呢的?來看看吧:
我們觀察發現,在我們的entry chunk數量沒有發生變化的時候,改變一個entry chunk的內容導致runtime內容發生變化的只有chunk id這個時候問題就又來了。根據上面穩定module id的操作一樣,數值型的chunk id不穩定性太大,我們要換,方式和上面一樣。
- 找到穩定chunk id的方式
- 找到改變chunk id的時機
7、穩定chunk id的相關操作
找到穩定chunk id的方式
因為我們知道webpack在打包的時候入口是具有唯一性的,那麼很簡單我們能不能夠用入口對應的name呢?所以這裡就比較簡單了我們就用我們的entry name來替換chunk id。
找到改變chunk id的時機
根據經驗module 有上面的過程那麼 chunk我覺得也是有的。
before-chunk-ids
-> optimize-chunk-ids
-> after-optimize-chunk-ids
所以編寫外掛:
'use strict'
class chunkIDsByFilePath {
constructor(options) {}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('before-chunk-ids', chunks => {
chunks.forEach(chunk => {
chunk.id = chunk.name
})
})
})
}
}
module.exports = chunkIDsByFilePath複製程式碼
不巧的是官方也有這個外掛所以不用我們寫。
NamedChunksPlugin複製程式碼
構建後的程式碼裡面我們可以看到了:
/******/ script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"ed00d7222262ac99e510","pageA":"b5b4e2893bce99fd5c57","pageB":"34be879b3374ac9b2072"}[chunkId] + ".js";複製程式碼
原來的chunk id現在全部變成了entry name了,變更的風險又小了一點了。美滋滋~~
我們換成名字後那麼問題又和上面module id換成name 又一樣的問題,檔案會變大。這個時候還是想到和上面的方式一樣用hash來處理。這個時候就真的要編寫外掛了。安利一波我們自己寫的
webpack-hashed-chunk-id-plugin。
到此持久化快取中遇到的核心難題都已經處理完了。
最後
如果你想要快速搭建一個專案,歡迎使用這邊的專案架構哦。
webpack-project-seed已經有線上專案用的用這個在跑了哦。順便star一個吧。
感謝:@pigcan