webpack长效缓存机制译

Posted 链家网技术团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了webpack长效缓存机制译相关的知识,希望对你有一定的参考价值。

如何正确的使用webpack长效缓存机制是一个始终没有得到答案的问题。

这个问题在Github上很受关注

  • 它有162条评论

  • 马上要到2周岁的生日了

  • 过多的建议往往使事情变得更糟糕

  • 还有一些在google中可以搜到

其实有一个简单的方式,就是使用RecordsPlugin插件(它的相关文档有点少)。但这需要记录你的每一个构建文件。我不喜欢依赖于状态,因此尝试给这个问题寻找一个好的答案。

  • 使用NamedModuleIds

  • 使用NamedChunkIds

  • 撒上一点魔法

  • 然后再来一点

但是首先最重要的,是什么阻止了默认情况下未优化webpack构建的长效缓存呢?说来话长了,让我们开始吧。

我们将使用webpack建立一个小程序,然后渐渐丰富它,从而在这过程中遇到各种各样的问题。就像解决其他好的问题一样,我们将全部解决它们。

基础

这是我们最初未优化的webpack配置:

// webpack.config.js 
const path = require('path'); 
const webpack = require('webpack'); 
module.exports = { 
  entry: { 
    main: './src/foo', 
  }, 
  output: { 
    path: path.join(__dirname, 'dist'), 
    filename: '[name].[hash].js', 
  }, 
};

这是 foo.js文件:

// foo.js 
import preact from 'preact'; 
console.log(preact.toString());

构建一下,我们得到如下的webpack输出:

Asset     Size  Chunks             Chunk Names
main.db3022283e4b37cce06b.js  23.6 kB       0  [emitted]  main
   [0] ./~/preact/dist/preact.js 20.5 kB {0} [built]
   [1] ./src/foo.js 61 bytes {0} [built]

到目前为止看起来还不错。

Vendor chunks

可能你想要做的第一件事就是将preact从主入口文件中移除,因为我们一般不会对它进行改动,所以我们在webpack配置中加入CommonsChunkPlugin 

// webpack.config.js 
// ... 
plugins: [ 
  new webpack.optimize.CommonsChunkPlugin({ 
    name: ['vendor'] 
  }) 
] 
// ...

再次构建一下,得到了一个类似的输出:

  Asset       Size  Chunks             Chunk Names
  main.423221edd7eef26d646e.js  506 bytes       0  [emitted]  main
vendor.423221edd7eef26d646e.js    26.7 kB       1  [emitted]  vendor
   [0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
   [1] ./src/foo.js 61 bytes {0} [built]
   [2] multi preact 28 bytes {1} [built]

不要睡着,好玩的才刚刚开始!

正确的哈希

你可能已经发现了,在这里我们遇到了第一个问题。mainvendor块的哈希是同步的。对main文件的任何修改都会使原来的vendor无效掉。

为了修复这个问题,我们必须将文件名中的hash 转换为 chunkhash。这是因为hash为我们构建的所有静态资源生成一些全局hash,而 chunkhash 只会为它块内的资源生成hash。

 // webpack.config.js 
// ... 
output: { 
  path: path.join(__dirname, 'dist'), 
  filename: '[name].[chunkhash].js', 
}, 
// ...

再次运行构建,现在我们看到两个不同的哈希。

 Asset       Size  Chunks             Chunk Names
  main.edc22f71759cbe5336ae.js  528 bytes       0  [emitted]  main
vendor.27f1230219fd2a606a54.js    26.7 kB       1  [emitted]  vendor
   [0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
   [1] ./src/foo.js 83 bytes {0} [built]
   [2] multi preact 28 bytes {1} [built]

非常棒!

运行时问题

现在在main中做任何修改应该不会影响到vendor了吧。让我们添加一行新的代码来测试一下:

// foo.js
// ...
console.log(preact.toString());
console.log("hello world");

再次构建,然而,我们看到所有的事情都白费了,同样的问题依然出现了:

Asset       Size  Chunks             Chunk Names
  main.91022729b32987083f0d.js  506 bytes       0  [emitted]  main
vendor.0da51f051fcf235d7027.js    26.7 kB       1  [emitted]  vendor
   [0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
   [1] ./src/foo.js 61 bytes {0} [built]
   [2] multi preact 28 bytes {1} [built]

可是这是为什么呢?问题的详细答案在这里:

因为Webpack在把 preactmain块中提取出来的同时,也将Webpack的运行时代码提取了出来。运行时代码是Webpack的一部分,用来解析模块、处理异步加载等。如果深入它的代码,在其中能看到我们主模块哈希的一个索引:

// somewhere in the vendor.0da51f051fcf235d7027.js
// ...
chunkId + "." + {"0":"91022729b32987083f0d"}[chunkId]
// ...

幸运的是我们可以解决这个问题。如果你添加一个CommonsChunkPlugin ,用一个入口文件名称中不存在的新名字,Webpack将生成一个以该名字命名的块,并提取运行时代码放入其中。听起来有没有很神奇?

// webpack.config.js 
// ... 
plugins: [ 
  // ... 
  new webpack.optimize.CommonsChunkPlugin({ 
    name: ['runtime'] 
  }) 
]

挥动着魔法的翅膀~再次构建,现在是这样的:

Asset       Size  Chunks             Chunk Names
 vendor.634878b098e5c599febd.js    20.7 kB       0  [emitted]  vendor
   main.d59c6ff3126e3943c563.js  538 bytes       1  [emitted]  main
runtime.25ce0c546aab71f8eac5.js    5.92 kB       2  [emitted]  runtime
   [0] ./~/preact/dist/preact.js 20.5 kB {0} [built]
   [1] ./src/foo.js 93 bytes {1} [built]
   [2] multi preact 28 bytes {0} [built]

现在更改main块中的内容将只会改变runtime和main块的哈希,vendor 块的哈希将保持不变。

添加更多的依赖

然而,故事还没有结束。随着项目丰富起来,我们来添加更多的依赖。

// foo.js
import bar from './bar';
// ...

再次构建,我们希望只有runtime和main块改变。但是,你应该猜到了,事实却并不是这样:

Asset       Size  Chunks             Chunk Names
   main.cec87b856171489c2719.js  811 bytes       0  [emitted]  main
 vendor.73db375ed475c718163f.js    20.7 kB       1  [emitted]  vendor
runtime.93b41beba42ebff23af0.js    5.92 kB       2  [emitted]  runtime
   [0] ./~/preact/dist/preact.js 20.5 kB {1} [built]
   [1] ./src/bar.js 23 bytes {0} [built]
   [2] ./src/foo.js 118 bytes {0} [built]
   [3] multi preact 28 bytes {1} [built]

虽然 vendor块中并没有任何更改,它的哈希却再一次改变了。这又是有原因的。每一个块都有一个数字的chunk id。这些chunk id是按顺序生成的。随着新的重要的代码添加,这顺序是会改变的,从而chunks ids也会随着改变。

命名你的chunk

加入 NamedChunksPlugin。它是在Webpack 2.4版本中新添加的,允许在块中使用名字而不是数字。

// weback.config.js 
// ... 
plugins: [ 
  new webpack.NamedChunksPlugin(), 
  // ... 
// ...

这将使用唯一的块名称而不是其id来标识一个块。

我们在添加和不添加bar.js 两种情况下分别再次构建,希望看到 vendor块的哈希保持不变。然而,结果并不是这样。分别观察两次构建后vendor块的代码,能够看到:

// old vendor build
// ...
/***/ }),
/* 1 */,
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
// ...


// vendor build with new import
// ...
/***/ }),
/* 1 */,
/* 2 */
/* 3 */
/***/ (function(module, exports, __webpack_require__) {

命名你的模块

由于一些原因,Webpack将存在的所有模块的id都加进了vendor块里面去。我们暂且不要太在乎为什么。幸运的是有一个简单的解决方案。加入NamedModulesPlugin

// webpack.config.js 
// ... 
plugins: [ 
  new webpack.NamedModulesPlugin(), 
  // ... 
// ...

它和命名块非常形似。它用一个唯一的路径去将我们的请求映射到一个模块,而不是用数字 id。

多亏了这个变化,现在vendor哈希将始终保持不变。

// output without bar.js

                          Asset       Size   Chunks             Chunk Names
   main.5f15e6808c8037c8bdbc.js  628 bytes     main  [emitted]  main
runtime.72ef2fc7d9df236c7f1c.js    5.94 kB  runtime  [emitted]  runtime
 vendor.73c86187abcdf9fd7b18.js    20.7 kB   vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
   [0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 121 bytes {main} [built]



// output with bar.js

                          Asset       Size   Chunks             Chunk Names
   main.47b747115cd1c2c24b93.js  901 bytes     main  [emitted]  main
runtime.8d94ab27ee79b53aa9a2.js    5.94 kB  runtime  [emitted]  runtime
 vendor.73c86187abcdf9fd7b18.js    20.7 kB   vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
   [0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 118 bytes {main} [built]

异步

随着我们应用程序的成长,它的体积越来越庞大。为了防止一次加载所有代码,我们使用一些异步分割点将其分解。首先,我们添加一个:

// foo.js
import('./async-bar').then( a => console.log(a));
// ...
 Asset       Size   Chunks             Chunk Names
      0.9110a255e8cbd547adc7.js  311 bytes        0  [emitted]
      ...

而不久之后又有一个异步依赖:

// foo.js
import('./async-bar').then( a => console.log(a));
import('./async-baz').then( a => console.log(a));
// ...
Asset       Size   Chunks             Chunk Names
      0.612f1fa751a3287cf615.js  311 bytes        0  [emitted]
      1.9606a5eadde08d70c763.js  311 bytes        1  [emitted]
      ...

WTF WEBPACK。为什么我的异步块的命名突然变化了?为什么他们再次编号?我认为将缓存无效是很难的,但你却总是在将它们无效掉:(。

事实上,NamedChunkPlugin只处理具有名称的块。我们的异步块并没有名称。愚蠢懒惰的OSS开发者。

我们来解决这个问题。

NamedChunksPlugin接收一个参数。此参数必须是一个函数,接收chunk作为其自身参数并且必须为它返回一个id。我们将插件更改为如下所示:

// webpack.config.js 
// ...
plugins: [ 
  // ... 
  new webpack.NamedChunksPlugin((chunk) => { 
    if (chunk.name) { 
      return chunk.name; 
    } 
    return chunk.modules.map(m => path.relative(m.context, m.request)).join("_"); 
  }), 
]

再次构建,我们现在可以根据需要添加多个异步块。以前添加的将保持其名称和hash不变:

Asset       Size        Chunks             Chunk Names
async-bar.js.06896d922ee7bb8af159.js  324 bytes  async-bar.js  [emitted]
async-baz.js.fff77d118cec24487e5d.js  324 bytes  async-baz.js  [emitted]
        main.16bb304a9926477df558.js    1.18 kB          main  [emitted]  main
     runtime.8f75ffddb5ac5820d4a9.js    6.01 kB       runtime  [emitted]  runtime
      vendor.73c86187abcdf9fd7b18.js    20.7 kB        vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
   [0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 218 bytes {main} [built]

旁注:请站在美学角度随意更改这些异步块的名称。我太懒了

现在一切都OK了吗?

如果我说并没有呢!

外部依赖

这是一个至今都没有死的事情,我们必须支持它。并且我们也可能在应用程序中真正使用到它。有些东西,我们不想加载它两次,并且想从全局上下文中得到它。但是,作为好的开发人员,我们也希望使依赖性显而易见。所以我们将jQuery定义为外部依赖。

// webpack.config.js 
// ... 
externals: { 
  jquery: 'jQuery' 
}, 
// ...

然后呢,有哪里可能会出现问题吗?好吧,所有的。

// before:

                               Asset       Size        Chunks             Chunk Names
async-bar.js.06896d922ee7bb8af159.js  324 bytes  async-bar.js  [emitted]
async-baz.js.fff77d118cec24487e5d.js  324 bytes  async-baz.js  [emitted]
        main.e4704e563c227ee88d16.js    1.21 kB          main  [emitted]  main
     runtime.6add1d75139c2dd504b1.js    6.01 kB       runtime  [emitted]  runtime
      vendor.73c86187abcdf9fd7b18.js    20.7 kB        vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
   [0] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 250 bytes {main} [built]



// after:

async-bar.js.06896d922ee7bb8af159.js  324 bytes  async-bar.js  [emitted]
async-baz.js.fff77d118cec24487e5d.js  324 bytes  async-baz.js  [emitted]
        main.f8b6bbf7315113f5b167.js    1.48 kB          main  [emitted]  main
     runtime.5544c7f42d217300ca94.js    6.01 kB       runtime  [emitted]  runtime
      vendor.3614a776703bbb63977c.js    20.7 kB        vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
    [1] multi preact 28 bytes {vendor} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]

好吧。。谢谢jQuery,干掉了我的vendor块?什么鬼?!

事实证明,就像chunks一样, NamedModulesPlugin也只适用于普通模块。也就是说,外部模块从 multi preact 模块中偷走 id 0。让我们一劳永逸地解决这个问题。

给每人一个名字

除了普通的模块,还有一堆Webpack使用的其他模块。然而并不是所有的都被NamedModulesPlugin覆盖。因此,我们需要自己处理(也许我应该把它做成一个包?*):

我们将其添加到我们的Webpack配置中:

// webpack.config.js 
plugins: [ 
  // ... 
  { 
    apply(compiler) { 
      compiler.plugin("compilation", (compilation) => { 
        compilation.plugin("before-module-ids", (modules) => { 
          modules.forEach((module) => { 
            if (module.id !== null) { 
              return; 
            } 
            module.id = module.identifier(); 
          }); 
        }); 
      }); 
    } 
  } 

现在在npm(https://www.npmjs.com/package/name-all-modules-plugin),是可用的哦

这其实与NamedModulesPlugin本身非常相似,不同的是,它为没有id的模块调用module#identifier方法。

再次构建,我们确实获得了一个新的vendor缓存,但这一次是稳定的了,无论你添加多少个外部模块!

Asset       Size        Chunks             Chunk
async-bar.js.06896d922ee7bb8af159.js  324 bytes  async-bar.js  [emitted]
async-baz.js.fff77d118cec24487e5d.js  324 bytes  async-baz.js  [emitted]
        main.8fad02c2ce285a92402a.js    1.52 kB          main  [emitted]  main
     runtime.a1ba8a5ef1cf9f245e87.js    6.01 kB       runtime  [emitted]  runtime
      vendor.12a52011916cc3cb208c.js    20.8 kB        vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
[multi preact] multi preact 28 bytes {vendor} [built]

正如你所看到的multi preactid,从[0]变为[multi preact]

就是这样了?不,还有最后一步!

添加更多的入口点

应用程序进一步扩大,我们需要有第二个入口。所以我们添加一个新的入口点,并将其添加到我们的Webpack配置中:

// webpack.config.js
// ...
entry: {
    main: './src/foo',
    other: './src/foo-two',
    vendor: ['preact']
},
// ...

它的内容是这样的:

// foo-two.js
import bar from './bar';
import preact from 'preact';
import('./async-baz').then( a => console.log(a));

console.log(preact.toString() + "hello world again and again");

再次构建,使我们无语了(╯°□°)╯(┻━┻。

现在构建后是这样:

 Asset       Size        Chunks             Chunk Names
async-bar.js.06896d922ee7bb8af159.js  324 bytes  async-bar.js  [emitted]
async-baz.js.fff77d118cec24487e5d.js  324 bytes  async-baz.js  [emitted]
        main.8407140a96b58ecd9a36.js    1.32 kB          main  [emitted]  main
       other.e86bb03c185f1e6bfc33.js  863 bytes         other  [emitted]  other
     runtime.da520a6c3b0dab33c4dc.js    6.05 kB       runtime  [emitted]  runtime
      vendor.c0d706bb45788764e72e.js      21 kB        vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {vendor} [built]
[./src/foo-two.js] ./src/foo-two.js 168 bytes {other} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
[multi preact] multi preact 28 bytes {vendor} [built]
    + 1 hidden modules

一切都变了。vendor,main文件,一切,但为什么?

事实证明,我们的vendor块不仅取决于我们指定的内容,还包括了两个入口点里面的共用的内容。虽然有时可能需要共用我们自己定义的常用模块,但这不应该归到vendor块中去。现在,我们根本就不想要这样。那么,添加minChunks: Infinity到我们的vendor公共块告诉Webpack我们真的只想要我们在条目中指定的内容:

// webpack.config.js
plugins: [
  //...
  new webpack.optimize.CommonsChunkPlugin({ 
    name: 'vendor', 
    minChunks: Infinity 
  }),
// ...

如果我们再次构建:

Asset       Size        Chunks             Chunk Names
async-bar.js.06896d922ee7bb8af159.js  324 bytes  async-bar.js  [emitted]
async-baz.js.fff77d118cec24487e5d.js  324 bytes  async-baz.js  [emitted]
        main.8fad02c2ce285a92402a.js    1.52 kB          main  [emitted]  main
       other.db28c745f0f45fa4d8ed.js    1.06 kB         other  [emitted]  other
     runtime.506eb2c73a3b28891b05.js    6.05 kB       runtime  [emitted]  runtime
      vendor.12a52011916cc3cb208c.js    20.8 kB        vendor  [emitted]  vendor
[./node_modules/preact/dist/preact.js] ./~/preact/dist/preact.js 20.5 kB {vendor} [built]
[./src/async-bar.js] ./src/async-bar.js 41 bytes {async-bar.js} [built]
[./src/async-baz.js] ./src/async-baz.js 41 bytes {async-baz.js} [built]
[./src/bar.js] ./src/bar.js 23 bytes {main} {other} [built]
[./src/foo-two.js] ./src/foo-two.js 168 bytes {other} [built]
[./src/foo.js] ./src/foo.js 247 bytes {main} [built]
[multi preact] multi preact 28 bytes {vendor} [built]

得到了我们想要的结果。最终我们的Webpack配置看起来是这样的:

const path = require('path');
const webpack = require('webpack');
const NameAllModulesPlugin = require('name-all-modules-plugin');

module.exports = {
    entry: {
        main: './src/foo',
        other: './src/foo-two',
        vendor: ['preact']
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].[chunkhash].js',
    },
    externals: {
        jquery: 'jQuery'
    },
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.NamedChunksPlugin((chunk) => {
            if (chunk.name) {
                return chunk.name;
            }
            return chunk.modules.map(m => path.relative(m.context, m.request)).join("_");
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: Infinity
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'runtime'
        }),
        new NameAllModulesPlugin(),
    ]
}

以上。至少这包含了我能够想到的所有边界情况。我希望这有帮助。如果您仍然发现有奇怪之处,请随时发表评论,我将尽力为他们寻求解决方案。

现在尽情去现实世界中享受吧。请停止使用奇怪的弊大于利的哈希插件吧!

最后一个重要的注意事项:您现在可能不得不关注缓存失效的问题。由于Webpack可能允许插件在chunkhash计算后更改静态资源,然而这些插件可能无法正确更新chunkhash,这可能导致静态资源保持其哈希不变,尽管实际上资源更改了。我个人没有碰到过这种情况,但注意一下总是好的。

就说这么多了!祝好!:)

以上是关于webpack长效缓存机制译的主要内容,如果未能解决你的问题,请参考以下文章

干货从构建进程间缓存设计 谈 Webpack5 优化和工作原理

webpack构建缓存机制-hash介绍

Windows7停止服务 奇安信三大场景解决方案和安全运营长效机制

译webpack文档翻译

[译]Webpack的奇妙世界

用 webpack 实现持久化缓存