资讯专栏INFORMATION COLUMN

基于webpack3的持久化缓存方案问题梳理

沈俭 / 1160人阅读

摘要:前言如何基于做持久化缓存目前感觉是一直没有一个非常好的方案来实践。另外所有资源的值保持一致,这对于所有资源的持久化缓存来说并没有深远的意义。到此持久化缓存中遇到的核心难题都已经处理完了。

前言

如何基于webpack做持久化缓存目前感觉是一直没有一个非常好的方案来实践。网上的文章非常多,但是真的有用的非常少,并没有一些真正深入研究和总结的文章。现在依托于于早教宝线上项目和自己的实践,有了一个完整的方案。

正文

1、webpack的hash的两种计算方式

想要做持久化缓存那么就要依赖 webpack 自身提供的两个 hashhashchunkhash

接着就来看看这两个值之间的具体含义和差别吧:

hash: webpack在每一次构建的时候都会产生一个compilation对象,这个hash值就是根据compilation内所有的内容计算而来的值。

chunkhash:这个值是根据每个chunk的内容而计算出来的值。

所以单纯根据上面的描述来说,chunkhash是用来做持久化缓存最有效的。

2、hash和chunkhash的测试

entry 入口文件 入口依赖
pageA a.js a.less->a.css, common.js->common.css
pageB b.js b.less->b.css, common.js->common.css

使用hash计算

const path = require("path")
const ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = {
  entry: {
    pageA: "./src/a.js",
    pageB: "./src/b.js"
  },
  output: {
    filename: "[name]-[hash].js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ["css-loader?minimize"]
        })
      }
    ]
  },
  plugins: [new ExtractTextPlugin("[name]-[hash].css")]
}

构建结果

Hash: 80c922b349f516e79fb5
Version: webpack 3.8.1
Time: 1014ms
                         Asset      Size  Chunks             Chunk Names
pageB-80c922b349f516e79fb5.js   2.86 kB       0  [emitted]  pageB
pageA-80c922b349f516e79fb5.js   2.84 kB       1  [emitted]  pageA
pageA-80c922b349f516e79fb5.css  21 bytes       1  [emitted]  pageA
pageB-80c922b349f516e79fb5.css  21 bytes       0  [emitted]  pageB

结论

可以发现所有文件的hash全部都是一样的,但是你多构建几次产生的hash都是不一样的。原因在于我们使用了 ExtractTextPluginExtractTextPlugin 本身涉及到异步的抽取流程,所以在生成 assets 资源时存在了不确定性(先后顺序),而 updateHash 则对其敏感,所以就出现了如上所说的 hash 异动的情况。另外所有 assets 资源的 hash 值保持一致,这对于所有资源的持久化缓存来说并没有深远的意义。

使用chunkhash计算

const path = require("path")
const ExtractTextPlugin = require("extract-text-webpack-plugin")
module.exports = {
  entry: {
    pageA: "./src/a.js",
    pageB: "./src/b.js"
  },
  output: {
    filename: "[name]-[chunkhash].js",
    path: path.resolve(__dirname, "dist")
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ["css-loader?minimize"]
        })
      }
    ]
  },
  plugins: [new ExtractTextPlugin("[name]-[chunkhash].css")]
}

构建结果

Hash: 810904f973cc0cf41992
Version: webpack 3.8.1
Time: 1038ms
                         Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-3a2e5ef3d4506fce8d93.css  21 bytes       1  [emitted]  pageA
pageB-e9ed5150262ba39827d4.css  21 bytes       0  [emitted]  pageB

结论

此时可以发现,运行多少次,hash 的变动没有了,每个 entry 拥有了自己独一的 hash 值,细心的你或许会发现此时样式资源的 hash 值和 入口脚本保持了一致,这似乎并不符合我们的想法,冥冥之中告诉我们发生了某些坏事情。

3、探索css文件的hash和入口文件hash之间的关系

在上面的构建结果中,我们发现css的hash值和入口文件的hash值是一样的,这里我们容易产生疑问,是不是这两个文件之间一定会有联系呢?呆着疑问去修改下b.css文件中的内容,产生构建结果:

Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1028ms
                         Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-3a2e5ef3d4506fce8d93.css  21 bytes       1  [emitted]  pageA
pageB-e9ed5150262ba39827d4.css  41 bytes       0  [emitted]  pageB

纳尼???改动css文件内容,为什么css文件的hash没有改变呢?不科学啊,入口文件的hash也没有改变。仔细想了一下 webpack 是将所有的内容都认为是js文件的一部分。在构建的过程中使用 ExtractTextPlugin 将样式抽离出entry chunk 了,而此时的 entry chunk 本身并没有发生改变,改变的是已经被抽离出去的css部分。而chunkunhash 却是根据 chunk 计算出来的,所以不变更应该是正常的。但是这个又不符合我们想要做的持久化缓存的要求,因为又变动就应该改变hash才是。

开心的是 ExtractTextPlugin 插件为我们提供了一个contenthash来变化:

  plugins: [new ExtractTextPlugin("[name]-[contenthash].css")]

修改b.css前后两次构建结果:

Hash: 3d95035f096f3ca08761
Version: webpack 3.8.1
Time: 1091ms
                     Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-9783744431577cdcfea658734b7db20f.css  21 bytes       1  [emitted]  pageA
pageB-2d03aa12ae45c64dedd7f66bb88dd3db.css  41 bytes       0  [emitted]  pageB
Hash: 7a96bcf1ef668a49c9d8
Version: webpack 3.8.1
Time: 1193ms
                     Asset      Size  Chunks             Chunk Names
pageB-e9ed5150262ba39827d4.js   2.86 kB       0  [emitted]  pageB
pageA-3a2e5ef3d4506fce8d93.js   2.84 kB       1  [emitted]  pageA
pageA-9783744431577cdcfea658734b7db20f.css  21 bytes       1  [emitted]  pageA
pageB-7e05e00e24f795b674df5701f6a38bd9.css  42 bytes       0  [emitted]  pageB

对比发现修改了样式文件后只有样式文件的hash发生了改变,符合我们想要的预期。

4、module id的不可控和修正

经过上面的测试,我们理所当然的认为我完成了持久化缓存的hash稳定。然后我们不小心删除了a.js中的a.less文件,然后前后两次构建:

Hash: 88ab71080c53db9d9f70
Version: webpack 3.8.1
Time: 1279ms
                                     Asset       Size  Chunks             Chunk Names
             pageB-a2d1e1d73336f17e2dc4.js    3.82 kB       0  [emitted]  pageB
             pageA-96c9f5afea30e7e09628.js     3.8 kB       1  [emitted]  pageA
pageA-d7ac82de795ddf50c9df43291d77b4c8.css   92 bytes       1  [emitted]  pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       0  [emitted]  pageB
Hash: 172153ea2b39c2046a92
Version: webpack 3.8.1
Time: 1260ms
                                     Asset       Size  Chunks             Chunk Names
             pageB-884da67fe2322246ab28.js    3.81 kB       0  [emitted]  pageB
             pageA-4c0dfb634722c556ffa0.js    3.68 kB       1  [emitted]  pageA
pageA-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       1  [emitted]  pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       0  [emitted]  pageB

奇怪的事产生了,我移除了a.less文件后发现pageB入口文件的hash都改变了。如果只有pageA相关的文件hash变了我还可以理解。但是????为什么都变了???不行我得看看为什么都变了。

通过上面的diff发现我们移除了a.less后整体的id发生了改变了。那么这个地方的id我们可以推测是代表的是具体的引用的模块。

接着我们在看看前后两次构建模块的信息:

[3] ./src/a.js 284 bytes {1} [built]
[4] ./src/a.less 41 bytes {1} [built]
[5] ./src/b.js 284 bytes {0} [built]
[6] ./src/b.less 41 bytes {0} [built]
[3] ./src/a.js 264 bytes {1} [built]
[4] ./src/b.js 284 bytes {0} [built]
[5] ./src/b.less 41 bytes {0} [built]

通过对比发现前面的序号在构建出来的pageB中有隐藏pageA相关的信息,这对于我们来做持久化缓存来说是非常不便的。我们期待的是pageB中只包含和自身相关的信息,不包含其他与自身无关的信息。

5、module id的变化

排除与己不相关的module id或者内容

会用webpack的人大概都之都一个特性:Code Splitting,本质上是对 chunk 进行拆分再组合的过程。具体要怎么做呢?

The answer is CommonsChunkPlugin,在plugin中添加:

plugins: [
    new ExtractTextPlugin("[name]-[contenthash].css"),
    new webpack.optimize.CommonsChunkPlugin({
      name: "runtime"
    })
]

接下来在看看移除pageA中的a.less的前后变化:

Hash: 697b36118920d991364a
Version: webpack 3.8.1
Time: 1488ms
                                       Asset       Size  Chunks             Chunk Names
               pageB-9b2eb6768499c911a728.js  491 bytes       0  [emitted]  pageB
               pageA-c342383ca09604e8e7b8.js  495 bytes       1  [emitted]  pageA
             runtime-b6ec3c0d350aef6cbf3e.js     6.8 kB       2  [emitted]  runtime
  pageA-b812cf5b72744af29181f642fe4dbf38.css   43 bytes       1  [emitted]  pageA
  pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css   59 bytes       0  [emitted]  pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]  runtime
Hash: 7ddaf109d5aa67c43ce2
Version: webpack 3.8.1
Time: 1793ms
                                       Asset       Size  Chunks             Chunk Names
               pageB-613cc5a6a90adfb635f4.js  491 bytes       0  [emitted]  pageB
               pageA-0b72f85fda69a9442076.js  375 bytes       1  [emitted]  pageA
             runtime-a41b8b8bfe7ec70fd058.js    6.79 kB       2  [emitted]  runtime
  pageB-af8f1e92fd031bd1d1d8db5390b5d0d5.css   59 bytes       0  [emitted]  pageB
runtime-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]  runtime

接着在看看两次构建中pageB的对比:

经过对比我们发现在pageB中只包含的是自身相关的内容。所以使用CommonsChunkPlugin达到了我们的期望。而抽离出去的代码就是webpack的运行时代码。运行时代码也存储着webpack对module和chunk相关的信息。另外我们发现pageA和pageB的文件大小也发生了变化。导致这个变化的原因是CommonsChunkPlugin会默认的把entry chunk都包含的module抽取到我们取名为runtime的normal chunk中去。

假如我们在开发中每个页面都会用到一些工具库,例如lodash这类的。由于CommonsChunkPlugin的默认行为会抽取公共部分,可能lodash并没有发生改变,但是被抽离在运行时代码中的时候,每次都是会去请求新的。这不能达到我们要求的最小更新原则。所以我们要人工去干预一些代码。

plugins: [
    new ExtractTextPlugin("[name]-[contenthash].css"),
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: Infinity
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: "runtime"
})

在次对边前后两次构建的日志:

Hash: a703a57c828ec32b24e1
Version: webpack 3.8.1
Time: 1493ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-f11f58b8150930590a10.js     541 kB       0  [emitted]  [big]  vendor
             pageB-7d065cd319176f44c605.js  938 bytes       1  [emitted]         pageB
             pageA-2b7e3707314e7ec4d770.js  910 bytes       2  [emitted]         pageA
           runtime-e68dec8bcad8a5870f0c.js    5.88 kB       3  [emitted]         runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css   92 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB
Hash: 26fc9ad18554b28cd8e1
Version: webpack 3.8.1
Time: 1806ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-d9bad56677b04b803651.js     541 kB       0  [emitted]  [big]  vendor
             pageB-a55dadfbf25a45856d6a.js  929 bytes       1  [emitted]         pageB
             pageA-7cbd77a502262ddcdd19.js  790 bytes       2  [emitted]         pageA
           runtime-fa8eba6e81ed41f50d6f.js    5.88 kB       3  [emitted]         runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB

到此为止我们解决了:排除与己不相关的module id或者内容问题。

稳定module id,尽可能的保持module id保持不变

一个module id是一个模块的唯一标示,并且该标示会出现在对应的entry chunk构建后的代码中。看个pageB的构建后代码的例子:

__webpack_require__(7)
const sum = __webpack_require__(0)
const _ = __webpack_require__(3)

根据前面的实验,模块的增加或者减少都会引起module id的改变,所以为了不引起module id的改变,那么我们只能找一个东西来代替module id作为标示。我们在构建的过程中就将寻找出来替代标示来替换module id。

所以上面的叙述可以转换成两个步骤来行动。

找到替代module id的方式

找到时机替换module id

6、稳定 module id 的相关操作

找到替代module id的方式
我们在日常的开发中,经常引用模块,都是通过地址来引用的。从这里我们可以得到启发,我们能不能够把module id全部替换成路径呢?再一个我们了解到在webpack resolve module阶段我们肯定是可以拿到资源路径的。在开始我们担心平台的路径差异性。幸运的是webpack 的源码其中在 ContextModule#74 和 ContextModule#35 中 webpack 对 module 的路径做了差异性修复。也就是说我们可以放心的通过module的libIdent来获取模块的路径了。

在整个webpack的执行过程中涉及到module id有三个钩子:

before-module-ids -> optimize-module-ids -> after-optimize-module-ids

所以我们只要在before-module-ids中做出修改就好了。

编写插件:

"use strict"

class moduleIDsByFilePath {
    constructor(options) {}

    apply(compiler) {
        compiler.plugin("compilation", compilation => {
            compilation.plugin("before-module-ids", (modules) => {
                modules.forEach((module) => {
                    if(module.id === null && module.libIdent) {
                        module.id = module.libIdent({
                            context: this.options.context || compiler.options.context
                        })
                    }
                })
            })
        })
    }
}

module.exports = moduleIDsByFilePath

上面的其实已经被webpack抽成一个插件了:

NamedModulesPlugin

所以只需要在插件那一部分里面添加上

new webpack.NamedModulesPlugin()

接下来对比下两次构建前后文件的变化:

Hash: e5bc78237ca9a3ad31f8
Version: webpack 3.8.1
Time: 1508ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-ebd9bfc583f45a344630.js     541 kB       0  [emitted]  [big]  vendor
             pageB-432105effc229524c683.js    1.09 kB       1  [emitted]         pageB
             pageA-158bf2a923c98ab49be2.js    1.09 kB       2  [emitted]         pageA
           runtime-9ca4cebe90e444e723b9.js    5.88 kB       3  [emitted]         runtime
pageA-d7ac82de795ddf50c9df43291d77b4c8.css   92 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB
Hash: 7dce5d9dc88f619522fe
Version: webpack 3.8.1
Time: 1422ms
                                     Asset       Size  Chunks                    Chunk Names
            vendor-ebd9bfc583f45a344630.js     541 kB       0  [emitted]  [big]  vendor
             pageB-432105effc229524c683.js    1.09 kB       1  [emitted]         pageB
             pageA-dae883ddaeff861761da.js  940 bytes       2  [emitted]         pageA
           runtime-c874a0c304fa03493296.js    5.88 kB       3  [emitted]         runtime
pageA-35be2c21107ce4016c324daaa1dd5e28.css   49 bytes       2  [emitted]         pageA
pageB-56185455ea60f01155a65497e9bf6c85.css  108 bytes       1  [emitted]         pageB

哇,我们对比发现只有相关改动的文件和运行时代码发生了改变,vendor和pageB相关都没有发生改变。美滋滋~~

这下我们达到了我们的目的,我们可以去看看我们构建后的代码了:

__webpack_require__("./src/b.less")
const sum = __webpack_require__("./src/common.js")
const _ = __webpack_require__("./node_modules/lodash/lodash.js")

真的是变成了路径,成功~~。但是新的问题貌似又来了,和之前的文件对比发现我们的文件普遍比之前的变大了。好吧,是我们换成文件路径的时候造成的。这个时候我们能不能用hash来代替文件路径呢?答案是可以,官方也有插件可以供我们使用:

new webpack.HashedModuleIdsPlugin()

官方说 NamedModulesPlugin 适合在开发环境,而在生产环境下请使用 HashedModuleIdsPlugin。
这样我们就达成了使用hash来代替原来的module id使之稳定。而且构建后的代码也不会变化太大。

本以为可以到此为止了。但是细心的人会发现runtime文件每次编译都发生了变化。是什么导致呢的?来看看吧:

我们观察发现,在我们的entry chunk数量没有发生变化的时候,改变一个entry chunk的内容导致runtime内容发生变化的只有chunk id这个时候问题就又来了。根据上面稳定module id的操作一样,数值型的chunk id不稳定性太大,我们要换,方式和上面一样。

找到稳定chunk id的方式

找到改变chunk id的时机

7、稳定chunk id的相关操作

找到稳定chunk id的方式

因为我们知道webpack在打包的时候入口是具有唯一性的,那么很简单我们能不能够用入口对应的name呢?所以这里就比较简单了我们就用我们的entry name来替换chunk id。

找到改变chunk id的时机

根据经验module 有上面的过程那么 chunk我觉得也是有的。

before-chunk-ids -> optimize-chunk-ids -> after-optimize-chunk-ids

所以编写插件:

"use strict"

class chunkIDsByFilePath {
    constructor(options) {}

    apply(compiler) {
        compiler.plugin("compilation", compilation => {
            compilation.plugin("before-chunk-ids", chunks => {
                chunks.forEach(chunk => {
                    chunk.id = chunk.name
                })
            })
        })
    }
}

module.exports = chunkIDsByFilePath

不巧的是官方也有这个插件所以不用我们写。

NamedChunksPlugin

构建后的代码里面我们可以看到了:

/******/         script.src = __webpack_require__.p + "" + chunkId + "-" + {"vendor":"ed00d7222262ac99e510","pageA":"b5b4e2893bce99fd5c57","pageB":"34be879b3374ac9b2072"}[chunkId] + ".js";

原来的chunk id现在全部变成了entry name了,变更的风险又小了一点了。美滋滋~~

我们换成名字后那么问题又和上面module id换成name 又一样的问题,文件会变大。这个时候还是想到和上面的方式一样用hash来处理。这个时候就真的要编写插件了。安利一波我们自己写的
webpack-hashed-chunk-id-plugin。

到此持久化缓存中遇到的核心难题都已经处理完了。

最后

如果你想要快速搭建一个项目,欢迎使用这边的项目架构哦。
webpack-project-seed已经有线上项目用的用这个在跑了哦。顺便star一个吧。

感谢:@pigcan

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90030.html

相关文章

  • 从零到千万用户云端(AWS)基础架构最佳实践

    摘要:本期大纲随着从到千万用户的业务增长,通过的不同服务轻松地实现高性能和高可用的基础架构。方坤老师本次的主题比较偏向实践的基础部分,假设了一个应用从小型到中型和大型的时候,可能需要用到的服务,以及相关介绍和实践建议。 极牛技术实践分享活动 极牛技术实践分享系列活动是极牛联合顶级VC、技术专家,为企业、技术人提供的一种系统的线上技术分享活动。每期不同的技术主题,和行业专家深度探讨,专注...

    ZHAO_ 评论0 收藏0
  • FE.BASE-HTTP基础梳理

    摘要:和是新加的,是对原状态码的细化。规定处理应是重定向为,处理应该是重定向为不一定是非请求即可和的存在,归根结底是由于方法的非幂等属性引起的。所以同时存在时,只有生效。超过该数值会有累积与端口耗尽问题。 前言 本文梳理本人阅读《HTTP权威指南》遇到的相关问题与相关解答。若有错误请指正。 OSI参考模型 应用层,表示层,会话层,传输层,网络层,数据链路层,物理层 URL ://:@:/;?...

    李文鹏 评论0 收藏0
  • webpack3 升级 webpack4踩坑记录

    摘要:本篇不包含所有坑,暂时只记录自己踩到的部分坑一安装安装最新版本安装新增依赖这个在中,本身和它的是在同一个包中,中将两个分开管理。我记录下自己更新这个的过程,以下前半部分可以直接跳过。以下记录踩坑过程。 本篇不包含所有坑,暂时只记录自己踩到的部分坑 一.安装 安装webpack4最新版本 npm install --save-dev webpack@4 安装新增依赖 webpack-c...

    马忠志 评论0 收藏0

发表评论

0条评论

沈俭

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<