Webpack

磨人的Webpack Hash

2018-11-13  本文已影响0人  记得要忘记_fca5

这是一篇废话连篇的文章。

从接触Webpack以来,自己是做内部系统为主,每次拿起chunkhash就是干,所以对Webpack的文件编译并没有太深入的研究。直到最近踩了几个坑之后,我才重新梳理了一下Webpack的hash

为什么要使用hash?

日常开发编译打包生成静态资源文件时,我们总是会利用文件名带上hash的方式,保证浏览器能够持久化缓存。更具体地解释就是我们希望达到这样一个目的:

相关代码没有发生变化时,尽可能地利用浏览器缓存,而不是频繁地请求静态资源服务器。

Webpack的hash类型

说hash之前,我们先抛出 Webapck 里面的两个概念 chunkmodule

image.png

简单地来说,一个或多个资源(js/css/img)组成module,一个或多个module又组成了chunk,其中包括entry chunknormal chunk。每个chunk最终生成一个file,就是我们的静态资源文件。也就是说,chunk最终都一个hash

Webpack作为时下最主流的业务代码编译打包工具,内置了以下三种hash处理方式:

hash是根据每次编译生成,chunkhash则是根据每个chunk的内容生成,contenthash用来对付css等其他资源。

由于我们的项目基本上都是多个entry(入口),如果每一次编译所有的文件都生成一个全新的hash,就会造成缓存的大量失效,这并不是我们期望的。我们最终想要达到的效果就是:

每当修改一个module时,只有引用到它的chunk才会更新对应的 hash

于是,chunkhash 脱颖而出了。


实际在使用chunkhash时,由于对webpack编译过程的不了解, chunkhash并没有像我期望的那样工作,这也让我踩坑不少。

接下来通过一个循序渐进的例子来展示chunkhash到底是个什么玩意儿。

准备数据

假设我们有入口文件 entry-a.js entry-b.js entry-cab 分别依赖 common-a.jscommon-b.js,三个入口文件都依赖 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',
  }

编译结果如下:

                              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

// common-a2.js
export default function () {
  console.log('I\'m common a2')
}

编译结果

                              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增加了一个依赖,只有entry-a的 hash 发生了变化,从d702a9dfe4bd9fd8d29e -> fe41f6501454aaba37deentry-bentry-c依然不变,完美!

王菲有一个歌叫《暗涌》,我个人一直非常喜欢,给大家推荐一下。

上面这个实验表面上是很成功,可到此为止了吗?实际上就像暗涌一下,表面平静,底下却潮水涌动。

为了方便对比hash的变化,我简单写了个plugin,去替代上面那种要对比两大坨编译结果才能定位到具体是哪个hash发生了变化。

// 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')
    })
  })
}

重复上面的操作后生成结果如下:

 entry-a: d702a9dfe4bd9fd8d29e -> fe41f6501454aaba37de
// entry-b.js
import cabc from './common-abc'

cabc()
console.log('I\'m entry b')

继续编译:

entry-a: fe41f6501454aaba37de -> 409266c0e175d92e5f40 
entry-b: e349f63455e20b60f6d5 -> 45eed8a58e4742f5c01d 
entry-c: f767774953520bfd7cea -> 3c651c9b9fa129486a53 

很遗憾,事情并没有跟我们想象的那样进行,仅仅是减少了entry-b的一个依赖之后,entry-aentry-chash也发生了变化。

原因其实很简单,contenthash是根据计算的,生成的文件内容发生了变化,计算出来的hash也就跟着变了。

那为什么在没有改变ac及其依赖模块的内容时,它们最终生成的文件hash也发生了变化。

每一个入口模块都会引入各个不同的被依赖模块,Webpack在编译文件时,会给所有的模块声明唯一的id,并生成一些函数,帮助入口模块去找到所有的依赖。

下面是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为几个被依赖模块分别生成了 module id 1 2 3 ...

结合webpack文档可以发现默认情况下module id 是根据模块的调用顺序,以数字自增的方式赋值的

如何保持module id的稳定性?


HashedModuleIdsPlugin是webpack内置的一个适用于生产环境的插件。它根据每个模块的相对路径计算出一个四个字符的hash串,解决了数值型id不稳定的问题。


修改一下webpack配置文件:

// webpack.config.js
// ...
plugins: [
  // ...
  new webpack.HashedModuleIdsPlugin()
]

重复上一个实验,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')

------------------------------------------------------------
// 编译结果
                              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')
------------------------------------------------
// 编译结果
                              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 

和我们期望的答案一样!(可以重复几次实验)

至此,收获持久化缓存第一招:

HashedModuleIdsPlugin

chunk

继续基于上面的实验

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')
}
---------------------------------------------------------------------------------------------- 
// 编译结果
                              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、在这个基础上再增加一个入口文件 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',
  },
----------------------------------------------------------------------------------------------
// 编译结果:
                               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 

本来我们期望的结果应该是这样的:

但上面的实验得到的答案却是:

重复多次上述实验会发现这样一个规律:

chunkmodule一样,默认以数字自增的方式为所有chunk分配一个id,每次增加或减少一个chunk,排在其后面的chunkid受到了影响,进而其hash也跟着发生了变化。

如何保持chunk id的稳定性?


namedChunks是webpack的一个解决这个问题的配置,它用chunkname替代了数字自增的方法为chunk id赋值,从而让chunk不受其他chunk id影响。

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

重复前面的实验 test -3:

// 原始编译结果

                              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')
}
---------------------------------------------------------------------------------------------- 
// 编译结果
                              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 

从编译结果可以看到,增加了async之后,只有引入它的entry-a发生了hash变化,其他的chunk保持不变。

2、在这个基础上再增加一个入口文件 entry-a2

// entry-a2.js
export default function () {
  console.log('I\'m entry a2')
}

----------------------------------------------------------------------------------------------
// 编译结果:
                               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 

这个编译结果依然符合我们的期望,增加了一个全新的entry,已存在的所有chunk都不会受到影响。

多重复几次实验,执行结果依然符合期望。

在这里,收获持久化缓存第二招:

optimization.namedChunks: true

webpack文档里其实对这个配置的定义是便于开发模式下调试,所以在development模式下该配置默认是true,而在production下则相反。这里我其实是比较费解,仅仅是因为namedChunk生成的chunk id比默认的numeric idsize稍大一点,就降低了chunk id的稳定性,但其带来的所谓size的精简在硕大的工程里简直是无足轻重,感觉有点舍本逐末。

另外,在webpack 5之后,namedChunks将会变成一个deprecated配置,取而代之的是optimization.chunkIds: named

总结

在一大堆无聊的实验之,得到以下结论

webpack优化的方式其实还有很多,自己动手踩坑,看一下webpack生成后的代码还有官方文档,总是能发现并解决问题。就好比我做完上述实验,又发现了一个问题,等着下次解决吧。

上一篇下一篇

猜你喜欢

热点阅读