cache-loadder工作原理

51 阅读5分钟

cache-loader 的工作流程

deepseek_mermaid_20251027_4d20c8.png

缓存文件对应参数详细解释:

主要参数说明

  1. remainingRequest

    • 说明:表示当前处理的文件路径和loader链,这是一个style类型的处理请求,包含了处理CSS内容的loader链
  2. dependencies

    • 类型:数组,包含8个依赖项
    • 每个依赖项包含两个字段:
      • path :依赖文件的绝对路径
      • mtime :文件的修改时间戳
    • 主要依赖包括:
      • 源文件:orderInfo.vue
      • loader文件:cache-loader、css-loader、stylePostLoader、postcss-loader、vue-loader、thread-loader
  3. contextDependencies

    • 说明:上下文依赖,当前没有额外的上下文依赖
  4. 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

##工作原理 工作步骤如下:

  1. pitch阶段

    1. 获取到remainingRequest参数,会根据该参数生成唯一的hash,并通过该hashcache-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)有关

    1. 读取通过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的路径。

  • 它的依赖dependenciescontextDependenciespathmtime。其中依赖的mtime是该缓存文件生成时,这个依赖被修改的时间

  • 文件的内容Result,通过buffer-json转换后的base64格式数据

    1. 缓存文件是否修改的判断。根据dependenciescontextDependencies文件的path依次读取依赖,将每个依赖实际的mtime与缓存文件中存储的mtime做对比,当每个依赖的mtime都相等时,会在pitch阶段直接返回缓存文件的result内容,并通过this.addDependencythis.addContextDependency将缓存文件记录的依赖添加到loader中(因为pitch阶段返回内容后,不执行normal execution),使得依赖更改时会更新文件。 如果不满足,则会记录当前时间startTime。供后续使用data.startTime = Date.now();

    normal execution阶段
    4. cache-loader中,该阶段是为了生成缓存文件。首先,通过getDependenciesgetContextDependencies获取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)