背景:
想把node_modules/.cache目录上传到一个中心化仓库,然后在CI过程构建的时候,可以拉下来重复利用。但是实践中,虽然.cache的确拉下来了,业务代码也一点没变,但是缓存就是没有生效。探究其原因,最后发现是cache-loader的基于时间(mtime)的缓存验证机制带来的问题。
cache-loader如何工作
cache-loader作为webpack的loader,会在pitch和loader两个阶段分别做一些事情:
pitch阶段:校验缓存文件是否可用
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文件,这个文件主要主要有两部分内容:
当前正在处理的文件所依赖的文件
当前正在处理的文件,在上一次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存入到缓存中。