Open soda-x opened 7 years ago
感谢博主~我想问下有没有能实现output出来的文件,某些带hash,某些不带hash的解决方案?
@Dcatfly 可以有方案,可以在 assets 资源输出磁盘之前的任何时机进行更名即可,写个 plugin 就好,不过选择时机你需要斟酌一下,比如 emit 那肯定是经过了 hash 的运算而造成计算上的浪费。
@pigcan 好的,谢谢博主~这么一想确实是可以的,而且跟我现在做的事情是类似的。。😆
非常好的文章,对利用webpack做持久化缓存有了更加深刻的认识,带着问题的探索模式值得学习
你好,问一下,关于chunk那块,你改了某一个entry文件的内容,并不只是chunkId变了啊,在runtime中的那个文件的hash(script中的引用)也变了啊,也会导致runtime的hash变化吧。
@suihanoooooo runtime 我没记错的话最后的理想态应该是引入的 entry chunk 的 hash 值,和内容不相关,当然是不涉及 entry chunk 条目等变更
webpack4在async chunk这一块的处理上相较3有了不少变化,不妨针对4再重新探讨一下
再次不经意间,拜读到了大佬的文章👍👍👍
很棒的文章,解决了我的很多疑惑
探究精神很赞
2021年事情有什么变化了吗
如何基于 webpack 做持久化缓存似乎一直处于没有最佳实践的状态。网路上各式各样的文章很多,open 的 bug 反馈和建议成堆,很容易让人迷茫和心智崩溃。
TL;DR;
拉到最后看总结 XD
hash 的两种计算方式
想要做持久化缓存的首要一步是 hash,在 webpack 中提供了两种方式,
hash
和chunkhash
在此或许有不少同学就这两者之间的差别就模糊了:
hash
:在 webpack 一次构建中会产生一个 compilation 对象,该 hash 值是对 compilation 内所有的内容计算而来的,chunkhash
:每一个 chunk 都根据自身的内容计算而来。单从上诉描述来看,
chunkhash
应该在持久化缓存中更为有效。到底是否如此呢,接下来我们设定一个应用场景。
设定场景
common.js <- common.less <- common.css
lodash
common.js <- common.less <- common.css
lodash
hash
时:构建结果:
如果细心一点,多尝试几次,可以发现即使在全部内容未变动的情况下 hash 值也会发生变更,原因在于我们使用了 extract,extract 本身涉及到异步的抽取流程,所以在生成 assets 资源时存在了不确定性(先后顺序),而 updateHash 则对其敏感,所以就出现了如上所说的 hash 异动的情况。另外所有 assets 资源的 hash 值保持一致,这对于所有资源的持久化缓存来说并没有深远的意义。
chunkhash
时:构建结果:
此时可以发现,运行多少次,hash 的异动没有了,每个 entry 拥有了自己独一的 hash 值,细心的你或许会发现此时样式资源的 hash 值和 入口脚本保持了一致,这似乎并不符合我们的想法,冥冥之中告诉我们发生了某些坏事情。
然后尝试随意修改
b.css
然后重新构建得到以下日志,不可思议的恐怖的事情发生了,居然 PageB 脚本和样式的 hash 值均未发生改变。为什么?细想一下不难理解,因为在 webpack 中所有的内容都视为 js 的一部分,而当构建发生,extract 生效后,样式被抽离出 entry chunk,此时对于 entry chunk 来说其本身并未发生改变,因为改变的部分已经被抽离变成 normal chunk,而 chunkhash 是根据 chunk 内容而来,所以不变更应该是符合预期的行为。虽然原理和结果符合预期,但是这并不是持久化缓存所需要的。幸运的是,extract-text-plugin 为抽离出来的内容提供了
contenthash
即:new ExtractTextPlugin('[name]-[contenthash].css')
此时我们再修改
b.css
然后重新构建得到以下日志,很棒!一切符合预期,只有 pageB 的样式 hash 发生了变更。你以为事情都结束了,然而总是会一波三折
接下来我们尝试在
a.js
中除去依赖a.less
,再进行一次构建,得到以下日志奇怪的事情再次发生,这边我们可以理解 pageA 的脚本和样式发生变化。但是对于 pageB 的脚本也发生变化感觉并不符合预期。
所以我们 pageB.js 去看一看到底是什么发生了变更。
通过如下命令我们可以获知具体的变更位置
结果为:
以上我们可以明确的知道,当 pageA 内移除 a.less 后整体的 id 发生了变更。那么可以推测的是 id 代表着具体的引用的模块。
其实在构建结束时,webpack 会给到我们具体的每个模块分配到的 id 。
case: pageA 移除 a.less 前
case: pageA 移除 a.less 后
通过比较发现,在 pageA 移除 a.less 的依赖前,居然在其构建出来的代码中,隐藏着
/* 73 */,
和/* 74 */,
,也就是说 pageB 的脚本中包含着a.js
,a.less
的模块 id 信息。这对于持久化来说并不符合预期。我们期待的是 pageB 中不会包含任何和它并不相关的内容。这边衍生出两个命题
module id 异动
我们来一个一个看。
命题1:如何把不相关的 module id 或者说内容摒除在外
简单来说,我们的目标就是把这些不相关的内容摒除在 pageA 和 pageB 的 entry chunk 之外。
对 webpack 熟悉的人或多或少听说过 Code Splitting,本质上是对 chunk 进行拆分再组合的过程。那谁能完成此任务呢?
相信你已经猜到了 -
CommonsChunkPlugin
接下来我们回退所有之前的变更。来检验我们的猜测是否正确。
在构建配置中我们加上
CommonsChunkPlugin
case: pageA 移除 a.less 前
case: pageA 移除 a.less 后
此时我们再通过如下命令
对 pageB 的脚本来进行对比
发现模块的内容终于不再包含和 pageB 不相关的其他的内容。换言之
CommonsChunkPlugin
达到了我们的预期,其实这部分内容即是 webpack 的 runtime,他存储着 webpack 对 module 和 chunk 的信息。另外有趣的是 pageA 和 pageB 在尺寸上也有了惊人的减小,原因在于默认行为的CommonsChunkPlugin
会把 entry chunk 都包含的 module 抽取到这个名为 runtime 的 normal chunk 中。在持久化缓存中我们的目标是力争变更达到最小化。但是在如上两次变更中不难发现我们仅仅是变更了 pageA 但是 runtime pageB pageA 却都发生了变更,另外由于 runtime 中由于CommonsChunkPlugin
的默认行为抽取了 lodash,我们有充分的理由相信 lodash 并未更新但却需要花费高昂的代价去更新,这并不符合最小化原则。所以在这边需要谈到的另外一点便是
CommonsChunkPlugin
的用法并不仅仅局限于自动化的抽取,在持久化缓存的背景下我们也需要人为去干预这部分内容,真正意义上去抽取公共内容,并尽量保证后续不再变更。在这里需要再迈出一步去自定义公共部分的内容。注意
runtime
要放在最后!我们再对所有的变更进行回退。再来看看是否会满足我们的期望!
case: pageA 移除 a.less 前
case: pageA 移除 a.less 后
到此为止,合理利用
CommonsChunkPlugin
我们解决了命题 1命题2:如何能让 module id 尽可能的保持不变
module id 是一个模块的唯一性标识,且该标识会出现在构建之后的代码中,如以下 pageB 脚本片段
模块的增减肯定或者引用权重的变更肯定会导致 id 的变更(这边对 id 如何进行分配不做展开讨论,如有兴趣可以以 webpack@1 中的 OccurrenceOrderPlugin 作为切入,该插件在 webpack@2 中被默认内置)。所以不难想象如果要解决这个问题,肯定是需要再找一个能保持唯一性的内容,并在构建期间进行 id 订正。
所以命题二被拆分成两个部分。
找到替代数值型 module id 方式
直觉的第一反应肯定是路径,因为在一次构建中资源的路径肯定是唯一的,另外我们也可以非常庆幸在 webpack 中肯定在 resolve module 的环节中拿到资源的路径。
不过谈到路径,我们不得不担忧一下,windows 和 macos 下路径的 sep 是不一致的,如果我们把 id 生成这一块单独拿出来自己做了,会不会还要处理一大堆可能存在的差异性问题。带着这样的困惑我查阅了 webpack 的源码其中在 ContextModule#74 和 ContextModule#35 中 webpack 对 module 的路径做了差异性修复。
也就是说我们可以放心的通过 module 的 libIdent 方法来获取模块的路径
找到时机进行 id 订正
时机就不是难事了,在 webpack 中我一直认为最 NB 的地方在于其整体插件的实现全部基于它的 tapable 事件系统,在灵活性上堪称完美。事件机制这部分内容我会在后续着重写文章分享。
这边我们只需要知道的是,在整个 webpack 执行过程中涉及 moudle id 的事件有
before-module-ids
->optimize-module-ids
->after-optimize-module-ids
所以我们只需要在
before-module-ids
这个时机内进行 id 订正即可。实现 module id 稳定
这部分内容,已经被 webpack 抽取为一个内置插件 NamedModulesPlugin
所以只需一小步在构建配置中添加该插件即可
回滚之前所有的代码修改,我们再来做相应的比较
case: pageA 移除 a.less 前
case: pageA 移除 a.less 后
自此利用
NamedModulesPlugin
我们做到了 pageA 中的变更只引发了 pageA 的脚本、样式、和 runtime 的变更,而 vendor,pageB 的脚本和样式均未发生变更。一窥 pageB 的代码片段
确实模块的 id 被替换成了模块的路径。但是不得不规避的问题是,尺寸变大了,因为 id 数字 和 路径的字符数不是一个量级,以 vendor 为例,应用方案前后尺寸上增加了
16KB
。或许有同学已经想到,那我对路径做次 hash 然后取几位不就得了,是的没错,webpack 官方就是这么做的。NamedModulesPlugin
适合在开发环境,而在生产环境下请使用 HashedModuleIdsPlugin。所以在生产环境下,为了获得最佳尺寸我们需要变更下构建的配置
在生产环境下把 NamedModulesPlugin 替换为 HashedModuleIdsPlugin,在包的尺寸增加幅度上上达到了可接受的范围,以 vendor 为例,只增加了 1KB。
事情到此我以为可以结束了,直到我 diff 了一下 runtime 才发现持久化缓存似乎还可以继续深挖。
我们发现在 3 个 entry 入口未改变的情况下,变更某个 entry chunk 的内容,对应 runtime 脚本的变更只是涉及到了 chunk id 的变更。基于 module id 的经验,自然想到了是不是有相应的唯一性内容来取代现有的 chunk id,因为数值型的 chunk id 总会存在不确定性。
所以至此问题又再次被拆分成两个命题:
chunk id 的不稳定性
接下来我们一个一个看
命题1:找到替代现有 chunk id 表达唯一性的方式
因为我们知道在 webpack 中 entry 其实是具有唯一性的,而 entry chunk 的 name 即来源于我们对 entry 名的设置。所以这里的问题变得很简单我们只需要把每个 chunk 对应的 id 指向到对应 chunk 的 name 即可。
命题2:找到时机进行 chunk id 订正
在整个 webpack 执行过程中涉及 moudle id 的事件有
before-chunk-ids
->optimize-chunk-ids
->after-optimize-chunk-ids
所以我们只需要在
before-chunk-ids
这个时机内进行 chunk id 订正即可。伪代码:
非常简单。
在 webpack@2 时期作者把这个部分的实现引入到了官方插件,即
NamedChunksPlugin
。所以在一般需求下我们只需要在构建配置中添加
NamedChunksPlugin
的插件即可。runtime 的 diff
可以看到标示 chunk 唯一性的 id 值被替换成了我们 entry 入口的名称。非常棒!感觉出岔子的机会又减小了不少。
讨论这个问题的另外一个原因是像 webpack@2 中的 dynamic import 或者 webpack@1 时的 require.ensure 会将代码抽离出来形成一个独立的 bundle,在 webpack 中我们把这种行为叫成 Code Splitting,一旦代码被抽离出来,最终在构建结果中会出现 0.[hash].js 1.[hash].js ,或多或少大家对此都有过困扰。
可以预想的是通过该 plugin 我们能比较好解决这个问题,一方面我们可以尝试定义这些被动态加载的模块的名称,另外一方面我们也可以遇见,假定一个构建场景会生成多个 [chunk-id].[chunkhash].js, 当 Code Splitting 的 chunk 需要变更时,比如减少了一个,此时你没法保证在新一个 compilation 中还继续分配到上一个 compilation 中的 [chunk-id],所以通过 name 命名的方式恰好可以顺带解决这个问题。
只是在这边我们需要稍微对
NamedChunksPlugin
做一些变更。总结
要做到持久化缓存需要做好以下几点:
[chunkhash]
对 extractTextPlugin 应用的的文件应用[contenthash]
;CommonsChunkPlugin
合理抽出公共库vendor
(包含社区工具库这些 如 lodash), 如果必要也可以抽取业务公共库common
(公共部分的业务逻辑),以及 webpack的runtime
;NamedModulesPlugin
来固化 module id,在生产环境下使用HashedModuleIdsPlugin
来固化 module idNamedChunksPlugin
来固化 runtime 内以及在使用动态加载时分离出的 chunk 的 chunk id。