CI环境下cache-loader的局限性以及修复方式

4,179 阅读3分钟

背景:

想把node_modules/.cache目录上传到一个中心化仓库,然后在CI过程构建的时候,可以拉下来重复利用。但是实践中,虽然.cache的确拉下来了,业务代码也一点没变,但是缓存就是没有生效。探究其原因,最后发现是cache-loader的基于时间(mtime)的缓存验证机制带来的问题。

cache-loader如何工作

cache-loader作为webpack的loader,会在pitch和loader两个阶段分别做一些事情:

  1. pitch阶段:校验缓存文件是否可用

  2. loader阶段:判断当前loader的文件是否需要重新生成缓存

pitch和loader与DOM的capture和pop很像,假设有这样一个loader配置:

代码块
loader: ['cache-loader', 'vue-loader']

那么pitch阶段的处理流程是:cache-loader -> vue-loader,而loader阶段的处理流程是:vue-loader -> cache-loader。并且,在这两个阶段可以通过一个共享的data对象来传递消息

pitch阶段

根据当前正在处理的文件,读取.cache目录中对应的cache文件,这个文件主要主要有两部分内容:

  1. 当前正在处理的文件所依赖的文件

  2. 当前正在处理的文件,在上一次loader过程中的产物

其中第1点用来判断当前文件的缓存是否依然有效,如果判断有效,那么就直接复用第2点的内容。

loader阶段

在这个阶段,我们要判断当前文件是否需要重新生成缓存,判断逻辑很简单:

如果pitch阶段的判断当前文件的缓存失效了,那么loader阶段就要去生成缓存。

局限性

在CI环境中,多次部署之间是隔离的,也就是说node_modules中的所有文件每次都会重新生成,所以node_modules/.cache目录自然就丢失了。

那我们自然会想到一个办法:在编译后把node_modules/.cache存到云上,然后在下次编译的时候再来下来。的确,这个方案是没问题的,

但是在实施的过程中,却遇到了一个问题:上述先存再拉的流程确实执行了,但是cache-loader还是会重新生成缓存,并没有利用上。

基于这个现象去看了cache-loader代码后发现,在cache-loader的pitch阶段,它的“判断当前文件的缓存是否依然有效”的方法是:基于文件最后修改时间(mtime)来判断。

简单的来说就是:我们虽然从云上拉下来了上一次编译产生的缓存文件,与yarn重新安装的node_modules里的文件,存在mtime不一致问题,导致判断为“缓存失效”。

找到问题的原因后,翻了翻cache-loader的issues,发现大家也在吐槽只支持mtime判断这件事,但是cache-loader不打算推出新的判断方式,而是推荐大家等webpack5的多级缓存功能。

基于这个事实,我们只能去二次开发cache-loader,让它支持新的对比方式,从而解决上述问题。

cache-loader提供了自定义compare方法的接口,但是这个接口回调的参数无法让我们去获取文件hash,所以相当于是个摆设。

同时这个compare接口也已经上线了,再去改compare接口的回调参数,是一个breaking change,也不太现实。

新的对比方式

我们自然想到判断文件是否发生变动的方法:hash对比。因为hash不会随时间地点变化,它可以完美解决上述问题。具体方案是:

在pitch阶段使用hash对比判断缓存是否有效,如果缓存无效或没有缓存,接着在loader阶段读取文件并生成hash存入到缓存中。

具体代码:github.com/fxxjdedd/ca…