基于webpack4[.3+]构建可预测的持久化缓存方案

栏目: CSS · 发布时间: 5年前

内容简介:web缓存的好处不用多说,自从webpack一桶江湖后,如何做Predictable long term caching with Webpack让配置工程师们头疼不已。webpack4.3前,有相当多的文章介绍如何处理(见参考),这里想做些更到位的探索。不要放弃治疗:wine_glass:本文测试时候的一些版本:
本文针对的是`immutable content+long max-age`类型的web缓存。
校验缓存及service worker的处理方案后续有时间再更新。
复制代码

web缓存的好处不用多说,自从webpack一桶江湖后,如何做Predictable long term caching with Webpack让配置工程师们头疼不已。

webpack4.3前,有相当多的文章介绍如何处理(见参考),这里想做些更到位的探索。

问题

当业务开发完成,准备上线时,问题就来了 :

  1. 如何保证不同内容的资源拥有唯一的标识(hash值)?
  2. 修改了业务代码,重新打包,会不会导致所有资源的标识值都变动?
  3. 如果想稳定hash值,如何确保将变动的文件名降到最低?
  4. css/wasm等资源的变动,是否会影响chunk的哈希值?
  5. 业务中引用的顺序改变,是否会改变chunk的哈希值?是否应该?
  6. dynamic import的文件是否支持良好?
  7. 增删多个入口文件,是否会影响已有的哈希值?

不要放弃治疗:wine_glass:本文测试时候的一些版本:

Node.js: v10.8.0
Webpack: v4.17.1
复制代码

TL;DR

contenthash
HashedModuleIdsPlugin
HashedModuleIdsPlugin

需要长效缓存的资源

  • 图片、字体等media资源 media资源可以使用 file-loader 根据资源内容生成hash值,配合 url-loader 可以按需内联成base64格式,这里不多说。

  • css css资源如果不做特殊处理,会直接打进js文件中;生产环境我们通常会使用 mini-css-extract-plugin 抽取到单独的文件中或是内联。

  • js js文件的处理要麻烦的多,作为唯一的入口资源,js管理着其他module,引入了无穷无尽的疑问,这也是我们接下来的重点。

webpack4 hash类型

hash类型 描述
hash The hash of the module identifier
chunkhash The hash of the chunk
contenthash (webpack > 4.3.0) The hash of the content( only )

contenthash应该是一个比较重要的feature, webpack核心开发者认为这个可以完全替代chunkhash (见 issue#2096 ),也许会在webpack5中将contenthash改成 [hash]

那么他们的区别在哪里呢?

简单来说,当chunk中包含css、wasm时,如果css有改动,chunkhash也会发生改变,导致chunk的哈希值变动;如果使用contenthash, css的改动不会影响chunk的哈希值 ,因为它是依据chunk 的js内容生成的。

知道有这么几种就够了,下面就从最基本的例子开始吧:bicyclist:‍♂️。

栗子们

接下来都会在 production mode 下测试(如果你不清楚webpack4新增的mode模式,去翻翻webpack mode 文档吧)。

涉及到的拆包策略,会一笔带过,后续有时间再详细聊聊拆包相关的问题~

1. 简单的hash

最简单的配置文件如下:point_down:,

// webapck.config.js
const path = require('path'); 
const webpack = require('webpack'); 
module.exports = { 
    mode:'production',
    entry: { 
        index: './src/index.js', 
    }, 
    output: { 
        path: path.join(__dirname, 'dist'), 
        filename: '[name].[hash].js',
  }, 
};
复制代码

入口文件 index.js 很简单:

// index.js
console.log('hello webapck:frog:')
复制代码

打包结果:

基于webpack4[.3+]构建可预测的持久化缓存方案

这个例子使用了 name + hash 进行文件命名,因为hash是根据 module identifier 生成的,这意味着只要业务中有一点点小小的改动,hash值就会变,来看下面的例子。

2. 增加一个vendors

让我们来增加一点点复杂性。

@灰大 在 对Webpack的hash稳定性的初步探索 中展示了一个有趣的例子,我们也来试试看。

现在我们给入口文件增加一个a.js模块:

// index.js
import './a';
console.log('hello webpack:frog:');
复制代码

a模块引入了lodash中的identity方法:

// a.js
import {identity} from 'lodash';
identity();
复制代码

然后修改下webpack配置文件,以便抽出vendors文件及manifest。这里多说一句,runtimeChunk非常的小,同时可预见的并不会有体积上的大变,所以可以考虑内联进html。

// webapck.config.js
...
module.exports = { 
...
  // 使用splitChunks默认策略拆包,同时提取runtime
   optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all'
        }
    },
};
复制代码

打包结果是:

基于webpack4[.3+]构建可预测的持久化缓存方案

[hash] 的问题

相信你已经注意到了,上图打包后,所有的文件都具有相同的hash值,这意味着什么呢?

每一次业务迭代上线,用户端要重新接收静态资源, 因为hash值每次都会变动,之前的一切缓存都失效了 :grimacing:。

所以,我们想要做持久化缓存,肯定是不会用 [hash] 了。

3. chunkhash了解一下?

在webpack4.3之前,我们只能选择chunkhash进行模块标识,然而这个玩意儿如不是很稳,配置工程师们废了九牛二虎之力用了各种黑科技才将hash值尽可能的稳定。

新出的contenthash和chunkhash有多大的区别呢:flushed:?

来看下面几个例子。

使用chunkhash

我们将 [hash] 换成 [chunkhash] ,看下打包结果:

基于webpack4[.3+]构建可预测的持久化缓存方案

index、vendors和runtime都拥有了不同的哈希值, so far so good

我们继续灰大的例子,在index.js中增加b.js模块,b模块只有一行代码:

// index.js
import './b';  // 增加了b.js
import './a';

console.log('hello webpack:frog:');
复制代码
// b.js
console.log('no can no bb');
复制代码

打包结果:

基于webpack4[.3+]构建可预测的持久化缓存方案

index文件的哈希值变动符合预期,但是vendors的实质内容仍然是lodash包的identity方法,这个也变了就不能忍了。

原因是 webpack4默认按照resolving order使用自增id进行模块标识 ,所以插入了b.js导致vendors的id错后了一个数,这一点我们diff一下两个vendors文件就可以看出,两个文件只有这里不同:

基于webpack4[.3+]构建可预测的持久化缓存方案

灰大文章中也提到了,解决方案很简单,使用 HashedModuleIdsPlugin ,这是一个内置插件,它会根据模块路径生成模块id,问题就迎刃而解了:

(起初比较担心根据module path进行hash计算后命名,这样的方式是否会因操作系统不同而产生差异,毕竟已经吃过一次亏了,见 windows/linux下path路径不一致的问题 ,好在webpack官方已经处理过这个问题了,无需操心了)

// webpack.config.js
...
plugins:[
    new webpack.HashedModuleIdsPlugin({
        // 替换掉base64,减少一丢丢时间
        hashDigest: 'hex'
    }),
]
...
复制代码

(设置 optimization.moduleIds:'hash' 可以达到同样的效果,不过 需要webapck@4.16.0以上

打包结果:

// 有b模块时:
        index.a169ecea96a59afbb472.js  243 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.ec8eb4cb2ebdc83c76ed.js   1.42 KiB       2  [emitted]  runtime~index

// 没有b模块时:
        index.8296fb0301ada4a021b1.js  185 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.ec8eb4cb2ebdc83c76ed.js   1.42 KiB       2  [emitted]  runtime~index
复制代码

4. 增加一个css 模块

入口文件增加c.css:point_down:,c的内容不重要:

// index.js
import './c.css';
import './b';
import './a';
...
复制代码

配置一下 mini-css-extract-plugin 将这个css模块抽取出来:

// webpack.config.js
...
module: {
        rules: [
            {
                test: /\.css$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                use: [
                    {loader: MiniCssExtractPlugin.loader},
                    {loader: 'css-loader'}
                ]
            }
        ]
    },
plugins:[
    new webpack.HashedModuleIdsPlugin(),
    // 增加css抽取
    new MiniCssExtractPlugin({
        filename: '[name].[contenthash].css',
        chunkFilename: '[name].[contenthash].css'
    })
]
...
复制代码

然后打包。 改动一点c.css中的内容,再次打包。

这两次打包过程,我们 只对c.css文件做了改动 ,预期是什么呢? 当然是希望只有css文件的哈希值有改动 ,然而事情并不符合预期:

// 增加了c.css
                                Asset       Size  Chunks             Chunk Names
       index.90d7b62bebabc8f078cd.css   59 bytes       0  [emitted]  index
        index.e5d6f6e2219665941029.js  276 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.de3e5c92fb3035ae4940.js   1.42 KiB       2  [emitted]  runtime~index

// 改动c.css中的代码后
                                Asset       Size  Chunks             Chunk Names
       index.22b9c488a93511dc43ba.css   94 bytes       0  [emitted]  index
        index.704b09118c28427d4e8f.js  276 bytes       0  [emitted]  index
vendors~index.6b77ad9a953ec4f883b0.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.de3e5c92fb3035ae4940.js   1.42 KiB       2  [emitted]  runtime~index
复制代码

注意看index.js的哈希值:pushpin: 打包后,入口文件的哈希值竟然也变了,这就很让人头疼了。

5. contenthash治愈一切?

contenthash并不能解决moduleId自增的问题

使用contenthash和chunkhash,在上述vendors文件的行为上,有什么样的区别呢? 能否解决因模块变动的问题?

答案是不能:sweat_smile:。 毕竟文件内容中包含了变动的东西,还是需要 HashedModuleIdsPlugin 插件。

contenthash威力所在

contenthash可以解决的是,css模块修改后,js哈希值变动的问题。

修改配置文件:point_down::

...
    output: {
        path: path.resolve(__dirname, './dist'),
        // 改成contenthash
        filename: '[name].[contenthash].js'        
    },
...    
复制代码

直接来看对比:

// 增加了c.css
                                Asset       Size  Chunks             Chunk Names
       index.22b9c488a93511dc43ba.css   94 bytes       0  [emitted]  index
        index.41e5e160a222e08ed18d.js  276 bytes       0  [emitted]  index
vendors~index.ec19a3033220507df6ac.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.d25723c2af2e039a9728.js   1.42 KiB       2  [emitted]  runtime~index

// 改动c.css中的代码后
                                Asset       Size  Chunks             Chunk Names
       index.a4afb491e06f1bb91750.css   60 bytes       0  [emitted]  index
        index.41e5e160a222e08ed18d.js  276 bytes       0  [emitted]  index
vendors~index.ec19a3033220507df6ac.js   69.5 KiB       1  [emitted]  vendors~index
runtime~index.d25723c2af2e039a9728.js   1.42 KiB       2  [emitted]  runtime~index
复制代码

可以看到,index.js的chunk 哈希值在改动前后是完全一致的:100:。

6. 增加异步模块

为了优化首屏性能或是业务变得原来越臃肿时,我们不可避免的会进行一些异步模块的抽取和加载,通过dynamic import方式就很安逸。

然而,异步模块作为一个新的chunk,他的哈希值是啥样的嘞?

我们增加一个异步模块试试看。

// webpack.config.js
...
output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].[contenthash].js',
        // 增加chunkFilename
        chunkFilename: '[name].[contenthash].js'
},
...    
复制代码
// async-module.js
export default {
    content: 'async-module'
};


// index.js
import './c.css';
import './b';
import './a';
// 增加这个模块
import('./async-module').then(a => console.log(a));

console.log('hello webpack:frog:');
复制代码

async-module的内容也是不重要,重要的是增加这个模块前后的哈希值有了很大的变化! 没有异步模块:

基于webpack4[.3+]构建可预测的持久化缓存方案

增加异步模块:

基于webpack4[.3+]构建可预测的持久化缓存方案

再增加第二个异步模块:

基于webpack4[.3+]构建可预测的持久化缓存方案

上面的对比简直是一夜回到解放前。。。除了css文件的哈希值在线,其他的都发生了改变。

究其原因,是因为虽然我们稳定住了moduleId,但是对chunkId无能为力, 而且异步的模块因为没有chunk.name ,导致又使用了数字自增进行命名。

好在我们还有 NamedChunksPlugin 可以进行chunkId的稳定:point_down::

// webapck.config.js
...
plugin:{
      new webpack.NamedChunksPlugin(
            chunk => chunk.name || Array.from(chunk.modulesIterable, m => m.id).join("_")
     ),
        ...
}
...
复制代码

除此之外还有其他的方式可以稳定chunkId,不过由于或多或少的缺点在这里就不赘述了,来看现在打包的结果:

基于webpack4[.3+]构建可预测的持久化缓存方案

可以看出,异步模块也都有了name值,同时vendors的哈希值也回归了。

7. 增加第二个入口文件

在业务迭代过程中,经常会增删一些页面,那么这样的场景,哈希值是如何变化的呢?

// webpack.config.js
...
entry: {
        index: './src/index.js',
        index2: './src/index2.js'
    },
...    
复制代码

我们增加一个index2入口文件,内容是一句 console.log('i am index2~') ,来看打包结果:

基于webpack4[.3+]构建可预测的持久化缓存方案

可以看到,除了增加了index2.js和runtime~index2.js这两个文件外,其余文件的哈希值都没有变动,完美:wink:

原因是我们已经稳定住了ChunkId,各个chunks不会再根据resolving order进行数字自增操作了。

在实际生产环境中,当新引入的chunk依赖了其他公用模块时,还是会导致一些文件的哈希值变动,不过这个可以通过拆包策略来解决,这里就不赘述了。

总结

本文通过一些例子,总结了通过webpack4做长效缓存的原理以及踩坑实践,而且这些已经运用在了我们的实际业务中,对于频繁迭代的业务来说,有相当大的性能提升。

webpack4的长效缓存相比之前的版本有了很大的进步,也有些许不足,但是相信这些在webapck5中都会得到解决:ok_woman:‍♀️~


以上所述就是小编给大家介绍的《基于webpack4[.3+]构建可预测的持久化缓存方案》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java编程思想 (第4版)

Java编程思想 (第4版)

[美] Bruce Eckel / 陈昊鹏 / 机械工业出版社 / 2007-6 / 108.00元

本书赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握。 从本书获得的各项大奖以及来自世界各地的读者评论中,不难看出这是一本经典之作。本书的作者拥有多年教学经验,对C、C++以及Java语言都有独到......一起来看看 《Java编程思想 (第4版)》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具