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

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

内容简介: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+]构建可预测的持久化缓存方案》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Web Design for ROI

Web Design for ROI

Lance Loveday、Sandra Niehaus / New Riders Press / 2007-10-27 / USD 39.99

Your web site is a business--design it like one. Billions of dollars in spending decisions are influenced by web sites. So why aren't businesses laser-focused on designing their sites to maximize thei......一起来看看 《Web Design for ROI》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具