美文网首页Webpack
磨人的Webpack Hash

磨人的Webpack Hash

作者: 记得要忘记_fca5 | 来源:发表于2018-11-13 15:46 被阅读0次

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

    从接触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
      Using the unique hash generated for every build
    • chunkhash
      Using hashes based on each chunks' content
    • contenthash
      Using hashes generated for extracted content

    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

    • test1entry-a需要增加一个依赖common-a2
    // 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
    
    • test-2entry-b移除依赖 common-b,让entry-b只依赖于公共的模块common-abc
    // 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也发生了变化。

    • module id

    每一个入口模块都会引入各个不同的被依赖模块,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

    继续基于上面的实验

    • test-3 这个实验我们分两步进行

    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 
    

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

    • entry-a增加一个异步加载chunkentry-ahash发生变化,其他entry保持不变。
    • 增加一个全新的entry,已有的chunk(入口chunk/异步加载chunk)都应该保持不变。

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

    • 每次增加一个chunk,总是有部分毫不相干的chunk受到了影响。

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

    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-4namedChunks测试一下chunk id是否能保持稳定

    重复前面的实验 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

    总结

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

    • 给生成的文件名加入[chunkhash]
    • 使用HashedModuleIdsPluginmodule id保持稳定
    • 使用namedChunkschunk id保持稳定

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

    相关文章

      网友评论

        本文标题:磨人的Webpack Hash

        本文链接:https://www.haomeiwen.com/subject/wqzzxqtx.html