這是一篇廢話(huà)連篇的文章波势。
從接觸Webpack以來(lái),自己是做內(nèi)部系統(tǒng)為主,每次拿起
chunkhash
就是干清寇,所以對(duì)Webpack的文件編譯并沒(méi)有太深入的研究。直到最近踩了幾個(gè)坑之后护蝶,我才重新梳理了一下Webpack的hash
华烟。
為什么要使用hash?
日常開(kāi)發(fā)編譯打包生成靜態(tài)資源文件時(shí),我們總是會(huì)利用文件名帶上hash的方式持灰,保證瀏覽器能夠持久化緩存盔夜。更具體地解釋就是我們希望達(dá)到這樣一個(gè)目的:
相關(guān)代碼沒(méi)有發(fā)生變化時(shí),盡可能地利用瀏覽器緩存堤魁,而不是頻繁地請(qǐng)求靜態(tài)資源服務(wù)器喂链。
Webpack的hash類(lèi)型
說(shuō)hash之前,我們先拋出 Webapck 里面的兩個(gè)概念 chunk
和 module
妥泉。
簡(jiǎn)單地來(lái)說(shuō)椭微,一個(gè)或多個(gè)資源(js/css/img)組成module
,一個(gè)或多個(gè)module
又組成了chunk
盲链,其中包括entry chunk
和normal chunk
蝇率。每個(gè)chunk
最終生成一個(gè)file
,就是我們的靜態(tài)資源文件刽沾。也就是說(shuō)瓢剿,chunk
最終都一個(gè)hash
。
Webpack作為時(shí)下最主流的業(yè)務(wù)代碼編譯打包工具悠轩,內(nèi)置了以下三種hash處理方式:
-
hash
Using the unique hash generated for every build
-
chunkhash
Using hashes based on each chunks' content
-
contenthash
Using hashes generated for extracted content
hash是根據(jù)每次編譯生成间狂,chunkhash
則是根據(jù)每個(gè)chunk
的內(nèi)容生成,contenthash
用來(lái)對(duì)付css等其他資源火架。
由于我們的項(xiàng)目基本上都是多個(gè)entry
(入口)鉴象,如果每一次編譯所有的文件都生成一個(gè)全新的hash
,就會(huì)造成緩存的大量失效何鸡,這并不是我們期望的纺弊。我們最終想要達(dá)到的效果就是:
每當(dāng)修改一個(gè)
module
時(shí),只有引用到它的chunk
才會(huì)更新對(duì)應(yīng)的hash
骡男。
于是淆游,chunkhash
脫穎而出了。
實(shí)際在使用chunkhash
時(shí),由于對(duì)webpack
編譯過(guò)程的不了解犹菱, chunkhash
并沒(méi)有像我期望的那樣工作拾稳,這也讓我踩坑不少。
接下來(lái)通過(guò)一個(gè)循序漸進(jìn)的例子來(lái)展示chunkhash
到底是個(gè)什么玩意兒腊脱。
準(zhǔn)備數(shù)據(jù)
假設(shè)我們有入口文件 entry-a.js
entry-b.js
entry-c
访得,a
和 b
分別依賴(lài) common-a.js
和common-b.js
,三個(gè)入口文件都依賴(lài) common-abc.js
// entry-a.js
import ca from './common-a'
import cabc from './common-abc'
ca()
cabc()
console.log('I\'m entry a')
// entry-b.js
import cb from './common-b'
import cabc from './common-abc'
cb()
cabc()
console.log('I\'m entry b')
// entry-c.js
import cabc from './common-abc'
cabc()
console.log('I\'m entry c')
// common-a.js
export default function () {
console.log('I\'m common a')
}
// common-b.js
export default function () {
console.log('I\'m common b')
}
// common-abc.js
export default function () {
console.log('I am common-abc')
}
Webpack 配置如下:
// webpack.config.js
entry: {
'entry-a': './src/entry-a.js',
'entry-b': './src/entry-b.js',
'entry-c': './src/entry-c.js'
},
output: {
filename: '[name].[chunkhash].js',
chunkFilename: '[name].[chunkhash].js',
}
編譯結(jié)果如下:
Asset Size Chunks Chunk Names
entry-a.d702a9dfe4bd9fd8d29e.js 1.14 KiB 0 [emitted] entry-a
entry-b.e349f63455e20b60f6d5.js 1.14 KiB 1 [emitted] entry-b
entry-c.f767774953520bfd7cea.js 1.11 KiB 2 [emitted] entry-c
[0] ./src/common-abc.js 64 bytes {0} {1} {2} [built]
[1] ./src/entry-c.js 69 bytes {2} [built]
[2] ./src/entry-a.js + 1 modules 171 bytes {0} [built]
| ./src/entry-a.js 104 bytes [built]
| ./src/common-a.js 62 bytes [built]
[3] ./src/entry-b.js + 1 modules 171 bytes {1} [built]
| ./src/entry-b.js 104 bytes [built]
| ./src/common-b.js 62 bytes [built]
module
-
test1:
entry-a
需要增加一個(gè)依賴(lài)common-a2
// common-a2.js
export default function () {
console.log('I\'m common a2')
}
編譯結(jié)果
Asset Size Chunks Chunk Names
entry-a.fe41f6501454aaba37de.js 1.17 KiB 0 [emitted] entry-a
entry-b.e349f63455e20b60f6d5.js 1.14 KiB 1 [emitted] entry-b
entry-c.f767774953520bfd7cea.js 1.11 KiB 2 [emitted] entry-c
[0] ./src/common-abc.js 64 bytes {0} {1} {2} [built]
[1] ./src/entry-c.js 69 bytes {2} [built]
[2] ./src/entry-a.js + 2 modules 272 bytes {0} [built]
| ./src/entry-a.js 142 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[3] ./src/entry-b.js + 1 modules 171 bytes {1} [built]
| ./src/entry-b.js 104 bytes [built]
| ./src/common-b.js 62 bytes [built]
一切都很順利陕凹,entry-a
增加了一個(gè)依賴(lài)悍抑,只有entry-a
的 hash 發(fā)生了變化,從d702a9dfe4bd9fd8d29e
-> fe41f6501454aaba37de
杜耙,entry-b
和entry-c
依然不變搜骡,完美!
王菲有一個(gè)歌叫《暗涌》佑女,我個(gè)人一直非常喜歡浆兰,給大家推薦一下。
上面這個(gè)實(shí)驗(yàn)表面上是很成功珊豹,可到此為止了嗎?實(shí)際上就像暗涌一下榕订,表面平靜店茶,底下卻潮水涌動(dòng)。
為了方便對(duì)比hash
的變化劫恒,我簡(jiǎn)單寫(xiě)了個(gè)plugin贩幻,去替代上面那種要對(duì)比兩大坨編譯結(jié)果才能定位到具體是哪個(gè)hash
發(fā)生了變化。
// ChunkPlugin.js
...
MyChunkPlugin.prototype.apply = function (compiler) {
compiler.hooks.thisCompilation.tap('MyChunkPlugin', compilation => {
compilation.hooks.afterOptimizeChunkAssets.tap('MyChunkPlugin', chunks => {
const chunkMap = {}
chunks.forEach(chunk => (chunkMap[chunk.name] = chunk.renderedHash))
const result = fs.readFileSync('./hash.js', 'utf-8')
const diff = [];
if (result) {
const source = JSON.parse(result);
Object.keys(chunkMap).forEach(key => {
if (source[key] && chunkMap[key] !== source[key]) {
diff.push(`${key}: ${source[key]} -> ${chunkMap[key]} `)
} else {
diff.push(`${key}: '' -> ${chunkMap[key]} `)
}
})
}
fs.writeFile('./hash.js', `${JSON.stringify(chunkMap, null, '\t')}`)
fs.writeFile('./diff.js', diff.length ? diff.join('\n') : 'nothing changed')
})
})
}
重復(fù)上面的操作后生成結(jié)果如下:
entry-a: d702a9dfe4bd9fd8d29e -> fe41f6501454aaba37de
-
test-2:
entry-b
移除依賴(lài)common-b
两嘴,讓entry-b
只依賴(lài)于公共的模塊common-abc
// entry-b.js
import cabc from './common-abc'
cabc()
console.log('I\'m entry b')
繼續(xù)編譯:
entry-a: fe41f6501454aaba37de -> 409266c0e175d92e5f40
entry-b: e349f63455e20b60f6d5 -> 45eed8a58e4742f5c01d
entry-c: f767774953520bfd7cea -> 3c651c9b9fa129486a53
很遺憾丛楚,事情并沒(méi)有跟我們想象的那樣進(jìn)行,僅僅是減少了entry-b
的一個(gè)依賴(lài)之后憔辫,entry-a
和entry-c
的hash
也發(fā)生了變化趣些。
原因其實(shí)很簡(jiǎn)單,contenthash
是根據(jù)計(jì)算的贰您,生成的文件內(nèi)容發(fā)生了變化坏平,計(jì)算出來(lái)的hash
也就跟著變了。
那為什么在沒(méi)有改變a
和c
及其依賴(lài)模塊的內(nèi)容時(shí)锦亦,它們最終生成的文件hash
也發(fā)生了變化舶替。
- module id
每一個(gè)入口模塊都會(huì)引入各個(gè)不同的被依賴(lài)模塊,Webpack在編譯文件時(shí)杠园,會(huì)給所有的模塊聲明唯一的id顾瞪,并生成一些函數(shù),幫助入口模塊去找到所有的依賴(lài)。
下面是entry-a
在沒(méi)有壓縮混淆下的部分生成代碼:
//...
var _common_a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./common-a */ 1);
var _common_a2__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./common-a2 */ 2);
var _common_abc__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./common-abc */ 3);
//...
我們大概可以猜出Webpack為幾個(gè)被依賴(lài)模塊分別生成了 module id 1 2 3 ...
結(jié)合webpack文檔可以發(fā)現(xiàn)默認(rèn)情況下module id 是根據(jù)模塊的調(diào)用順序陈醒,以數(shù)字自增的方式賦值的惕橙。
如何保持module id
的穩(wěn)定性?
HashedModuleIdsPlugin是webpack內(nèi)置的一個(gè)適用于生產(chǎn)環(huán)境的插件孵延。它根據(jù)每個(gè)模塊的相對(duì)路徑計(jì)算出一個(gè)四個(gè)字符的hash串吕漂,解決了數(shù)值型id不穩(wěn)定的問(wèn)題。
修改一下webpack配置文件:
// webpack.config.js
// ...
plugins: [
// ...
new webpack.HashedModuleIdsPlugin()
]
重復(fù)上一個(gè)實(shí)驗(yàn)尘应,entry-b
依賴(lài) common-b
// entry-b.js
import cb from './common-b'
import cabc from './common-abc'
cb()
cabc()
console.log('I\'m entry b')
------------------------------------------------------------
// 編譯結(jié)果
Asset Size Chunks Chunk Names
entry-a.59fcd77ff264f62591d3.js 1.19 KiB 0 [emitted] entry-a
entry-b.408073538586b4495dd7.js 1.16 KiB 1 [emitted] entry-b
entry-c.1f28d5213db6b69b83ed.js 1.13 KiB 2 [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {0} {1} {2} [built]
[GUDB] ./src/entry-a.js + 2 modules 272 bytes {0} [built]
| ./src/entry-a.js 142 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[aIzb] ./src/entry-c.js 69 bytes {2} [built]
[grd8] ./src/entry-b.js + 1 modules 171 bytes {1} [built]
| ./src/entry-b.js 104 bytes [built]
| ./src/common-b.js 62 bytes [built]
去掉common-b
依賴(lài):
//entry-b.js
import cabc from './common-abc'
cabc()
console.log('I\'m entry b')
------------------------------------------------
// 編譯結(jié)果
Asset Size Chunks Chunk Names
entry-a.59fcd77ff264f62591d3.js 1.19 KiB 0 [emitted] entry-a
entry-b.a75ec2de235c6595507a.js 1.13 KiB 1 [emitted] entry-b
entry-c.1f28d5213db6b69b83ed.js 1.13 KiB 2 [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {0} {1} {2} [built]
[GUDB] ./src/entry-a.js + 2 modules 272 bytes {0} [built]
| ./src/entry-a.js 142 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[aIzb] ./src/entry-c.js 69 bytes {2} [built]
[grd8] ./src/entry-b.js 69 bytes {1} [built]
// diff
entry-b: 408073538586b4495dd7 -> a75ec2de235c6595507a
和我們期望的答案一樣;棠(可以重復(fù)幾次實(shí)驗(yàn))
至此,收獲持久化緩存第一招:
HashedModuleIdsPlugin
chunk
繼續(xù)基于上面的實(shí)驗(yàn)
- test-3 這個(gè)實(shí)驗(yàn)我們分兩步進(jìn)行
1犬钢、給entry-a
增加異步加載chunkasync.js
// entry-a.js
import ca from './common-a'
import ca2 from './common-a2'
import cabc from './common-abc'
ca()
ca2()
cabc()
(async function () {
const asy = await import(/* webpackChunkName: "async" */ './async')
asy()
})()
console.log('I\'m entry a')
// async.js
export default function () {
console.log('I am async')
}
----------------------------------------------------------------------------------------------
// 編譯結(jié)果
Asset Size Chunks Chunk Names
async.5411b81525bb7e4c771e.js 205 bytes 0 [emitted] async
entry-a.a9c2efa137c11a449854.js 9.45 KiB 1 [emitted] entry-a
entry-b.5f44a689594f78eb9b62.js 1.13 KiB 2 [emitted] entry-b
entry-c.220cbeddf5b77bf44a0d.js 1.13 KiB 3 [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {1} {2} {3} [built]
[GUDB] ./src/entry-a.js + 2 modules 961 bytes {1} [built]
| ./src/entry-a.js 821 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[TSF4] ./src/async.js 59 bytes {0} [built]
[aIzb] ./src/entry-c.js 69 bytes {3} [built]
[grd8] ./src/entry-b.js 69 bytes {2} [built]
+ 4 hidden modules
// diff.js
async: '' -> 5411b81525bb7e4c771e
entry-a: 59fcd77ff264f62591d3 -> a9c2efa137c11a449854
entry-b: a75ec2de235c6595507a -> 5f44a689594f78eb9b62
entry-c: 1f28d5213db6b69b83ed -> 220cbeddf5b77bf44a0d
2苍鲜、在這個(gè)基礎(chǔ)上再增加一個(gè)入口文件 entry-a2
:
// entry-a2.js
export default function () {
console.log('I\'m entry a2')
}
// webpack.config.js
entry: {
'entry-a': './src/entry-a.js',
'entry-a2': './src/entry-a2.js',
'entry-b': './src/entry-b.js',
'entry-c': './src/entry-c.js',
},
----------------------------------------------------------------------------------------------
// 編譯結(jié)果:
Asset Size Chunks Chunk Names
async.5411b81525bb7e4c771e.js 205 bytes 0 [emitted] async
entry-a.a9c2efa137c11a449854.js 9.45 KiB 1 [emitted] entry-a
entry-a2.cbf75fa37ffde273148a.js 1.04 KiB 2 [emitted] entry-a2
entry-b.ed39f7105ea4f26b42e3.js 1.13 KiB 3 [emitted] entry-b
entry-c.adebd02c1ec23be8edeb.js 1.13 KiB 4 [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {1} {3} {4} [built]
[GUDB] ./src/entry-a.js + 2 modules 961 bytes {1} [built]
| ./src/entry-a.js 821 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[PV30] ./src/entry-a2.js 62 bytes {2} [built]
[TSF4] ./src/async.js 59 bytes {0} [built]
[aIzb] ./src/entry-c.js 69 bytes {4} [built]
[grd8] ./src/entry-b.js 69 bytes {3} [built]
+ 4 hidden modules
// diff
entry-a2: '' -> cbf75fa37ffde273148a
entry-b: 5f44a689594f78eb9b62 -> ed39f7105ea4f26b42e3
entry-c: 220cbeddf5b77bf44a0d -> adebd02c1ec23be8edeb
本來(lái)我們期望的結(jié)果應(yīng)該是這樣的:
- 給
entry-a
增加一個(gè)異步加載chunk
,entry-a
的hash
發(fā)生變化玷犹,其他entry
保持不變混滔。 - 增加一個(gè)全新的
entry
,已有的chunk
(入口chunk
/異步加載chunk
)都應(yīng)該保持不變歹颓。
但上面的實(shí)驗(yàn)得到的答案卻是:
- 每次增加一個(gè)
chunk
坯屿,總是有部分毫不相干的chunk
受到了影響。
重復(fù)多次上述實(shí)驗(yàn)會(huì)發(fā)現(xiàn)這樣一個(gè)規(guī)律:
chunk
跟module
一樣巍扛,默認(rèn)以數(shù)字自增的方式為所有chunk
分配一個(gè)id
领跛,每次增加或減少一個(gè)chunk
,排在其后面的chunk
的id
受到了影響撤奸,進(jìn)而其hash
也跟著發(fā)生了變化吠昭。
如何保持chunk id
的穩(wěn)定性?
namedChunks是webpack的一個(gè)解決這個(gè)問(wèn)題的配置胧瓜,它用chunk
的name
替代了數(shù)字自增的方法為chunk id
賦值矢棚,從而讓chunk
不受其他chunk id
影響。
// webpack.config.js
module.exports = {
//...
optimization: {
namedChunks: true
}
};
-
test-4 用
namedChunks
測(cè)試一下chunk id
是否能保持穩(wěn)定
重復(fù)前面的實(shí)驗(yàn) test -3:
// 原始編譯結(jié)果
Asset Size Chunks Chunk Names
entry-a.0864367d249b191a3a0e.js 1.19 KiB entry-a [emitted] entry-a
entry-b.5c7b3532d418453241f4.js 1.13 KiB entry-b [emitted] entry-b
entry-c.6887e26445575eff0402.js 1.13 KiB entry-c [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {entry-a} {entry-b} {entry-c} [built]
[GUDB] ./src/entry-a.js + 2 modules 272 bytes {entry-a} [built]
| ./src/entry-a.js 142 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[aIzb] ./src/entry-c.js 69 bytes {entry-c} [built]
[grd8] ./src/entry-b.js 69 bytes {entry-b} [built]
1府喳、給entry-a
增加異步加載chunkasync.js
// entry-a.js
import ca from './common-a'
import ca2 from './common-a2'
import cabc from './common-abc'
ca()
ca2()
cabc()
(async function () {
const asy = await import(/* webpackChunkName: "async" */ './async')
asy()
})()
console.log('I\'m entry a')
// async.js
export default function () {
console.log('I am async')
}
----------------------------------------------------------------------------------------------
// 編譯結(jié)果
Asset Size Chunks Chunk Names
async.3b06cb8d92816f773b08.js 211 bytes async [emitted] async
entry-a.f201c1668ae5af4b9b59.js 9.47 KiB entry-a [emitted] entry-a
entry-b.5c7b3532d418453241f4.js 1.13 KiB entry-b [emitted] entry-b
entry-c.6887e26445575eff0402.js 1.13 KiB entry-c [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {entry-a} {entry-b} {entry-c} [built]
[GUDB] ./src/entry-a.js + 2 modules 961 bytes {entry-a} [built]
| ./src/entry-a.js 821 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[TSF4] ./src/async.js 59 bytes {async} [built]
[aIzb] ./src/entry-c.js 69 bytes {entry-c} [built]
[grd8] ./src/entry-b.js 69 bytes {entry-b} [built]
+ 4 hidden modules
// diff
async: '' -> 3b06cb8d92816f773b08
entry-a: 0864367d249b191a3a0e -> f201c1668ae5af4b9b59
從編譯結(jié)果可以看到蒲肋,增加了async
之后,只有引入它的entry-a
發(fā)生了hash
變化钝满,其他的chunk
保持不變肉津。
2、在這個(gè)基礎(chǔ)上再增加一個(gè)入口文件 entry-a2
:
// entry-a2.js
export default function () {
console.log('I\'m entry a2')
}
----------------------------------------------------------------------------------------------
// 編譯結(jié)果:
Asset Size Chunks Chunk Names
async.3b06cb8d92816f773b08.js 211 bytes async [emitted] async
entry-a.f201c1668ae5af4b9b59.js 9.47 KiB entry-a [emitted] entry-a
entry-a2.820dc92f91bed3570102.js 1.04 KiB entry-a2 [emitted] entry-a2
entry-b.5c7b3532d418453241f4.js 1.13 KiB entry-b [emitted] entry-b
entry-c.6887e26445575eff0402.js 1.13 KiB entry-c [emitted] entry-c
[F85t] ./src/common-abc.js 64 bytes {entry-a} {entry-b} {entry-c} [built]
[GUDB] ./src/entry-a.js + 2 modules 961 bytes {entry-a} [built]
| ./src/entry-a.js 821 bytes [built]
| ./src/common-a.js 62 bytes [built]
| ./src/common-a2.js 63 bytes [built]
[PV30] ./src/entry-a2.js 62 bytes {entry-a2} [built]
[TSF4] ./src/async.js 59 bytes {async} [built]
[aIzb] ./src/entry-c.js 69 bytes {entry-c} [built]
[grd8] ./src/entry-b.js 69 bytes {entry-b} [built]
+ 4 hidden modules
// diff
entry-a2: '' -> 820dc92f91bed3570102
這個(gè)編譯結(jié)果依然符合我們的期望舱沧,增加了一個(gè)全新的entry
妹沙,已存在的所有chunk
都不會(huì)受到影響。
多重復(fù)幾次實(shí)驗(yàn)熟吏,執(zhí)行結(jié)果依然符合期望距糖。
在這里玄窝,收獲持久化緩存第二招:
optimization.namedChunks: true
在webpack
文檔里其實(shí)對(duì)這個(gè)配置的定義是便于開(kāi)發(fā)模式下調(diào)試,所以在development
模式下該配置默認(rèn)是true
悍引,而在production
下則相反恩脂。這里我其實(shí)是比較費(fèi)解,僅僅是因?yàn)?code>namedChunk生成的chunk id
比默認(rèn)的numeric id
的size
稍大一點(diǎn)趣斤,就降低了chunk id
的穩(wěn)定性俩块,但其帶來(lái)的所謂size
的精簡(jiǎn)在碩大的工程里簡(jiǎn)直是無(wú)足輕重,感覺(jué)有點(diǎn)舍本逐末浓领。
另外玉凯,在webpack 5之后,namedChunks
將會(huì)變成一個(gè)deprecated
配置联贩,取而代之的是optimization.chunkIds: named
漫仆。
總結(jié)
在一大堆無(wú)聊的實(shí)驗(yàn)之,得到以下結(jié)論
- 給生成的文件名加入
[chunkhash]
- 使用
HashedModuleIdsPlugin
讓module id
保持穩(wěn)定 - 使用
namedChunks
讓chunk id
保持穩(wěn)定
webpack優(yōu)化的方式其實(shí)還有很多泪幌,自己動(dòng)手踩坑盲厌,看一下webpack生成后的代碼還有官方文檔,總是能發(fā)現(xiàn)并解決問(wèn)題祸泪。就好比我做完上述實(shí)驗(yàn)吗浩,又發(fā)現(xiàn)了一個(gè)問(wèn)題,等著下次解決吧没隘。