這是一篇廢話連篇的文章。
從接觸Webpack以來,自己是做內(nèi)部系統(tǒng)為主,每次拿起
chunkhash就是干,所以對Webpack的文件編譯并沒有太深入的研究。直到最近踩了幾個(gè)坑之后,我才重新梳理了一下Webpack的hash。
為什么要使用hash?
日常開發(fā)編譯打包生成靜態(tài)資源文件時(shí),我們總是會(huì)利用文件名帶上hash的方式,保證瀏覽器能夠持久化緩存。更具體地解釋就是我們希望達(dá)到這樣一個(gè)目的:
相關(guān)代碼沒有發(fā)生變化時(shí),盡可能地利用瀏覽器緩存,而不是頻繁地請求靜態(tài)資源服務(wù)器。
Webpack的hash類型
說hash之前,我們先拋出 Webapck 里面的兩個(gè)概念 chunk 和 module。

簡單地來說,一個(gè)或多個(gè)資源(js/css/img)組成module,一個(gè)或多個(gè)module又組成了chunk,其中包括entry chunk和normal chunk。每個(gè)chunk最終生成一個(gè)file,就是我們的靜態(tài)資源文件。也就是說,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用來對付css等其他資源。
由于我們的項(xiàng)目基本上都是多個(gè)entry(入口),如果每一次編譯所有的文件都生成一個(gè)全新的hash,就會(huì)造成緩存的大量失效,這并不是我們期望的。我們最終想要達(dá)到的效果就是:
每當(dāng)修改一個(gè)
module時(shí),只有引用到它的chunk才會(huì)更新對應(yīng)的hash。
于是,chunkhash 脫穎而出了。
實(shí)際在使用chunkhash時(shí),由于對webpack編譯過程的不了解, chunkhash并沒有像我期望的那樣工作,這也讓我踩坑不少。
接下來通過一個(gè)循序漸進(jìn)的例子來展示chunkhash到底是個(gè)什么玩意兒。
準(zhǔn)備數(shù)據(jù)
假設(shè)我們有入口文件 entry-a.js entry-b.js entry-c,a 和 b 分別依賴 common-a.js和common-b.js,三個(gè)入口文件都依賴 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è)依賴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è)依賴,只有entry-a的 hash 發(fā)生了變化,從d702a9dfe4bd9fd8d29e -> fe41f6501454aaba37de,entry-b和entry-c依然不變,完美!
王菲有一個(gè)歌叫《暗涌》,我個(gè)人一直非常喜歡,給大家推薦一下。
上面這個(gè)實(shí)驗(yàn)表面上是很成功,可到此為止了嗎?實(shí)際上就像暗涌一下,表面平靜,底下卻潮水涌動(dòng)。
為了方便對比hash的變化,我簡單寫了個(gè)plugin,去替代上面那種要對比兩大坨編譯結(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移除依賴common-b,讓entry-b只依賴于公共的模塊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
很遺憾,事情并沒有跟我們想象的那樣進(jìn)行,僅僅是減少了entry-b的一個(gè)依賴之后,entry-a和entry-c的hash也發(fā)生了變化。
原因其實(shí)很簡單,contenthash是根據(jù)計(jì)算的,生成的文件內(nèi)容發(fā)生了變化,計(jì)算出來的hash也就跟著變了。
那為什么在沒有改變a和c及其依賴模塊的內(nèi)容時(shí),它們最終生成的文件hash也發(fā)生了變化。
- module id
每一個(gè)入口模塊都會(huì)引入各個(gè)不同的被依賴模塊,Webpack在編譯文件時(shí),會(huì)給所有的模塊聲明唯一的id,并生成一些函數(shù),幫助入口模塊去找到所有的依賴。
下面是entry-a在沒有壓縮混淆下的部分生成代碼:
//...
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è)被依賴模塊分別生成了 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è)模塊的相對路徑計(jì)算出一個(gè)四個(gè)字符的hash串,解決了數(shù)值型id不穩(wěn)定的問題。
修改一下webpack配置文件:
// webpack.config.js
// ...
plugins: [
// ...
new webpack.HashedModuleIdsPlugin()
]
重復(fù)上一個(gè)實(shí)驗(yàn),entry-b依賴 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依賴:
//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
本來我們期望的結(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è)問題的配置,它用chunk的name替代了數(shù)字自增的方法為chunk id賦值,從而讓chunk不受其他chunk id影響。
// webpack.config.js
module.exports = {
//...
optimization: {
namedChunks: true
}
};
-
test-4 用
namedChunks測試一下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í)對這個(gè)配置的定義是便于開發(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)定性,但其帶來的所謂size的精簡在碩大的工程里簡直是無足輕重,感覺有點(diǎn)舍本逐末。
另外,在webpack 5之后,namedChunks將會(huì)變成一個(gè)deprecated配置,取而代之的是optimization.chunkIds: named。
總結(jié)
在一大堆無聊的實(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)并解決問題。就好比我做完上述實(shí)驗(yàn),又發(fā)現(xiàn)了一個(gè)問題,等著下次解決吧。