cache-loader 的工作流程
缓存文件对应参数详细解释:
主要参数说明
-
remainingRequest
- 说明:表示当前处理的文件路径和loader链,这是一个style类型的处理请求,包含了处理CSS内容的loader链
-
dependencies
- 类型:数组,包含8个依赖项
- 每个依赖项包含两个字段:
- path :依赖文件的绝对路径
- mtime :文件的修改时间戳
- 主要依赖包括:
- 源文件:orderInfo.vue
- loader文件:cache-loader、css-loader、stylePostLoader、postcss-loader、vue-loader、thread-loader
-
contextDependencies
- 说明:上下文依赖,当前没有额外的上下文依赖
-
result
- 类型:数组,包含2个对象
- 第一个对象:
- type : "Buffer"
- data : "base64:DQouZWwtZGVzY3JpcHRpb25zX19ib2R5IC5lbC1kZXNjcmlwdGlvbnNfX3RhYmxlIC5lbC1kZXNjcmlwdGlvbnNfX2NlbGwuaXMtbGVmdDpoYXMoLmNsYVNlbGZ0KXsNCiAgZGlzcGxheTpmbGV4ICFpbXBvcnRhbnQ7DQogIHdpZHRoOjIwMCU7DQp9DQo="
- 说明:编译后的CSS内容(vue,js内容等也是这样格式),以base64编码存储
- 第二个对象:
- version :3(Source Map版本)
- sources : ["orderInfo.vue"] (源码文件)
- names : [] (映射的变量名)
- mappings : ";AA4KA;AACA;AACA;AACA" (源码位置映射信息)
- file : "orderInfo.vue" (文件名)
- sourceRoot : "src/viewspe/shopOrder/shopOrderDetail/components" (源码根路径)
- sourcesContent :包含完整的原始Vue文件内容
cache-loader的作用为将文件处理的结果缓存,在下次构建时,如果文件没变化,则直接获取缓存的内容
需要注意的是,cache-loader中源文件的内容都是由webpack获取后,通过参数获得的,并不是在loader中通过fs.read读取
复制代码
function loader(content) {
console.log('文件内容Buffer',content)
}
module.exports = loader
##工作原理 工作步骤如下:
-
pitch阶段
- 获取到
remainingRequest参数,会根据该参数生成唯一的hash,并通过该hash与cache-loader的缓存目录生成文件的缓存路径cacheKey,然后将这两个参数挂载到data中,便于normal execution阶段读取
const findCacheDir = require('find-cache-dir'); const cacheIdentifier = `cache-loader:${pkg.version} ${env}` // cacheDirectory一般是: node_modules/.cache/cache-loader const cacheDirectory = findCacheDir({ name: 'cache-loader' }) || os.tmpdir() function pitch(remainingRequest, prevRequest, dataInput){ const hash = digest(`${cacheIdentifier}\n${remainingRequest}`) // 向normal execution注入数据 dataInput.remainingRequest = remainingRequest dataInput.cacheKey = path.join(cacheDirectory, `${hash}.json`) }可以看到,
cacheKey的生成与文件的内容无关,与文件的路径以及cache-loader的版本、环境(developmeng / production)有关- 读取通过
cacheKey对应的缓存文件内容,如果文件不存在或读取失败,则结束pitch阶段。否则,根据缓存文件的内容,判断是否使用缓存。
其中缓存文件内容结构如下:
- 获取到
// 缓存文件内容结构示例
{
"remainingRequest": "/Users/maiguoheng/Desktop/code/dict-course-class/node_modules/babel-polyfill/node_modules/core-js/modules/es7.weak-map.from.js",
"dependencies": [
{
"path": "/Users/maiguoheng/Desktop/code/dict-course-class/node_modules/babel-polyfill/node_modules/core-js/modules/es7.weak-map.from.js",
"mtime": 499162500000
},
{
"path": "/Users/maiguoheng/Desktop/code/dict-course-class/tiny-cache-loader.js",
"mtime": 1698907103551
}
],
"contextDependencies": [],
"result": [
{
"type": "Buffer",
"data": "base64:Ly8gaHR0cHM6Ly90YzM5LmdpdGh1Yi5pby9wcm9wb3NhbC1zZXRtYXAtb2Zmcm9tLyNzZWMtd2Vha21hcC5mcm9tCnJlcXVpcmUoJy4vX3NldC1jb2xsZWN0aW9uLWZyb20nKSgnV2Vha01hcCcpOwo="
}
]
}
可以看到,缓存文件中存储了以下内容:
-
当前被处理文件
remainingRequest的路径。 -
它的依赖
dependencies与contextDependencies的path与mtime。其中依赖的mtime是该缓存文件生成时,这个依赖被修改的时间 -
文件的内容
Result,通过buffer-json转换后的base64格式数据- 缓存文件是否修改的判断。根据
dependencies与contextDependencies文件的path依次读取依赖,将每个依赖实际的mtime与缓存文件中存储的mtime做对比,当每个依赖的mtime都相等时,会在pitch阶段直接返回缓存文件的result内容,并通过this.addDependency与this.addContextDependency将缓存文件记录的依赖添加到loader中(因为pitch阶段返回内容后,不执行normal execution),使得依赖更改时会更新文件。 如果不满足,则会记录当前时间startTime。供后续使用data.startTime = Date.now();
normal execution阶段
4.cache-loader中,该阶段是为了生成缓存文件。首先,通过getDependencies与getContextDependencies获取webpapck处理后的依赖,并依次读取这些依赖的内容。如果读取的时候出错,那么将会结束缓存的处理,直接返回文件内容。其次,会对每一个依赖的mtime做比较const mtime = dependencyStats.mtime.getTime(); if (mtime / 1000 >= Math.floor(data.startTime / 1000)) { // Don't trust mtime. // File was changed while compiling // or it could be an inaccurate filesystem. cache = false; }这段代码的意思是,如果某个依赖在
pitch阶段之后被修改过(因为可能被编译),就不再缓存内容,如果上述校验都通过了,则生成缓存文件。其中,args是[content,map,meta],存储前文件的内容会由bufferJSON.stringify处理成文本 - 缓存文件是否修改的判断。根据
function loader(...args) {
const options = Object.assign({}, defaults, getOptions(this));
validateOptions(schema, options, {
name: 'Cache Loader',
baseDataPath: 'options'
});
const {
readOnly,
write: writeFn
} = options; // In case we are under a readOnly mode on cache-loader
// we don't want to write or update any cache file
if (readOnly) {
this.callback(null, ...args);
return;
}
const callback = this.async();
const {
data
} = this;
const dependencies = this.getDependencies().concat(this.loaders.map(l => l.path));
const contextDependencies = this.getContextDependencies(); // Should the file get cached?
let cache = true; // this.fs can be undefined
// e.g when using the thread-loader
// fallback to the fs module
const FS = this.fs || fs;
const toDepDetails = (dep, mapCallback) => {
FS.stat(dep, (err, stats) => {
if (err) {
mapCallback(err);
return;
}
const mtime = stats.mtime.getTime();
if (mtime / 1000 >= Math.floor(data.startTime / 1000)) {
// Don't trust mtime.
// File was changed while compiling
// or it could be an inaccurate filesystem.
//修改检测 :当文件的修改时间(mtime)大于等于编译开始时间(startTime)时,认为文件在编译过程中被修改
//缓存失效 :将 cache 标志设置为 false,表示不能信任当前状态下的缓存
//构建继续 :即使不缓存,构建流程也会继续,只是跳过了写入缓存的步骤
//下次重新编译 :由于没有写入新的缓存,下次构建时会重新处理该文件
cache = false;
}
mapCallback(null, {
path: pathWithCacheContext(options.cacheContext, dep),
mtime
});
});
};
async.parallel([cb => async.mapLimit(dependencies, 20, toDepDetails, cb), cb => async.mapLimit(contextDependencies, 20, toDepDetails, cb)], (err, taskResults) => {
if (err) {
callback(null, ...args);
return;
}
if (!cache) {
callback(null, ...args);
return;
}
const [deps, contextDeps] = taskResults;
writeFn(data.cacheKey, {
remainingRequest: pathWithCacheContext(options.cacheContext, data.remainingRequest),
dependencies: deps,
contextDependencies: contextDeps,
result: args
}, () => {
// ignore errors here
callback(null, ...args);
});
});
}
上面便是cache-loader详细的工作流程,是否使用缓存核心是依赖mtime的对比,另外就是通过buffer-json库将buffer文件内容转换成string(base64)类型。还有一点要注意的是,pitch阶段命中缓存时需要将记录到的dependencies添加到loader中,否则命中缓存后无法监听依赖的变化
部分内容引用[以cache-loader为例,了解loader运行流程loader执行顺序,与loader的pitch介绍 一个loa - 掘金](url)