磨人的Webpack Hash

這是一篇廢話連篇的文章。

從接觸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è)概念 chunkmodule。

image.png

簡單地來說,一個(gè)或多個(gè)資源(js/css/img)組成module,一個(gè)或多個(gè)module又組成了chunk,其中包括entry chunknormal 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,ab 分別依賴 common-a.jscommon-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

  • test1entry-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-bentry-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-2entry-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-aentry-chash也發(fā)生了變化。

原因其實(shí)很簡單,contenthash是根據(jù)計(jì)算的,生成的文件內(nèi)容發(fā)生了變化,計(jì)算出來的hash也就跟著變了。

那為什么在沒有改變ac及其依賴模塊的內(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-ahash發(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ī)律:

chunkmodule一樣,默認(rèn)以數(shù)字自增的方式為所有chunk分配一個(gè)id,每次增加或減少一個(gè)chunk,排在其后面的chunkid受到了影響,進(jìn)而其hash也跟著發(fā)生了變化。

如何保持chunk id的穩(wěn)定性?


namedChunks是webpack的一個(gè)解決這個(gè)問題的配置,它用chunkname替代了數(shù)字自增的方法為chunk id賦值,從而讓chunk不受其他chunk id影響。

// webpack.config.js
module.exports = {
  //...
  optimization: {
    namedChunks: true
  }
};

  • test-4namedChunks測試一下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 idsize稍大一點(diǎn),就降低了chunk id的穩(wěn)定性,但其帶來的所謂size的精簡在碩大的工程里簡直是無足輕重,感覺有點(diǎn)舍本逐末。

另外,在webpack 5之后,namedChunks將會(huì)變成一個(gè)deprecated配置,取而代之的是optimization.chunkIds: named。

總結(jié)

在一大堆無聊的實(shí)驗(yàn)之,得到以下結(jié)論

  • 給生成的文件名加入[chunkhash]
  • 使用HashedModuleIdsPluginmodule id保持穩(wěn)定
  • 使用namedChunkschunk id保持穩(wěn)定

webpack優(yōu)化的方式其實(shí)還有很多,自己動(dòng)手踩坑,看一下webpack生成后的代碼還有官方文檔,總是能發(fā)現(xiàn)并解決問題。就好比我做完上述實(shí)驗(yàn),又發(fā)現(xiàn)了一個(gè)問題,等著下次解決吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容