llunnn

2019-10-12 10:24

webpack源码阅读之Compiler

本文作者:IMWeb llunnn 原文出处:IMWeb社区 未经同意,禁止转载

本篇记录了阅读Compiler.js过程中的一些笔记。(Webpack版本4.41.0)

阅读前需要先对tapable有一定的了解,可参考Tapable github.

这里主要对webpack调用了Compiler.run()到资源输出完毕后经历的过程及其代码进行了一些梳理。

代码特点

webpack的异步代码基本采用回调函数的形式进行书写,tapable实际上也是注册callback的形式,需要仔细区分各个部分对应的callback。

Compiler类成员变量的类型、含义都比较清晰,也有足够的文档支持,这里不做具体解读了。

大致流程

Compiler中的方法调用顺序大致如下(以.run为入口):

Compiler.run(callback) 开始执行构建

Compiler.readRecord(callback) 读取之前的构建记录

Compiler.compile(callback) 进行编译

Compiler.newCompilationParams() 创建Compilation的参数

Compiler.newCompilation() 创建新的Compilation

Compiler.emitAssets(compilation, callback) 输出构建资源

Compiler.emitRecords(callback) 输出构建记录

源码阅读

Compiler.run(callback)

Compiler.run()是整个编译过程启动的入口,在lib/webpack.js中被调用。

// Compiler.run(callback)

run(callback) {
  // 如果编译正在进行,抛出错误(一个webpack实例不能同时进行多次编译)
  if (this.running) return callback(new ConcurrentCompilationError());

  // 定义运行结束的回调
    const finalCallback = (err, stats) => {
        this.running = false; // 正在运行的标记设为false
        if (err) {
      // 若有错误,执行failed钩子上的方法
      // 我们可以通过compiler.hooks.failed.tap()挂载函数方法
      // 其余hooks类似
            this.hooks.failed.call(err);
        }
        if (callback !== undefined) return callback(err, stats);
    };

    const startTime = Date.now();
  // 标记开始运行
  this.running = true;

  // 调用this.compile传入的回调函数
  const onCompiled = (err, compilation) => {
    // ...
  };

  // 执行beforeRun钩子上的方法
  this.hooks.beforeRun.callAsync(this, err => {
    if (err) return finalCallback(err);
        // 执行run钩子上的方法
    this.hooks.run.callAsync(this, err => {
      if (err) return finalCallback(err);

      // 读取之前的records
      this.readRecords(err => {
        if (err) return finalCallback(err);
                // 执行编译
        this.compile(onCompiled);
      });
    });
  });
}

Compiler.readRecord(callback)

readRecords用于读取之前的records的方法,关于records,文档的描述是pieces of data used to store module identifiers across multiple builds(一些数据片段,用于储存多次构建过程中的module的标识)可参考recordsPath

// Compiler.readRecord(callback)

readRecords(callback) {
  // recordsInputPath是webpack配置中指定的读取上一组records的文件路径
  if (!this.recordsInputPath) {
    this.records = {};
    return callback();
  }
  // inputFileSystem是一个封装过的文件系统,扩展了fs的功能
  // 主要是判断一下recordsInputPath的文件是否存在 存在则读取并解析,存到this.records中
  // 最后执行callback
  this.inputFileSystem.stat(this.recordsInputPath, err => {
    // It doesn't exist
    // We can ignore this.
    if (err) return callback();

    this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
      if (err) return callback(err);

      try {
        this.records = parseJson(content.toString("utf-8"));
      } catch (e) {
        e.message = "Cannot parse records: " + e.message;
        return callback(e);
      }

      return callback();
    });
  });
}

Compiler.compile(callback)

compile是真正进行编译的过程,创建了一个compilation,并将compilation传给make钩子上的方法,注册在这些钩子上的函数方法会调用compilation上的方法,执行构建。在compilation结束(finish)和封装(seal)完成后,便可以执行传入回调,也就是在Compile.run()中定义的的onCompiled函数。

// Compiler.compile(callback)

compile(callback) {
  // 创建了compilation的初始参数
  const params = this.newCompilationParams();
  // 执行beforeCompile钩子上的方法
  this.hooks.beforeCompile.callAsync(params, err => {
    if (err) return callback(err);

    // 执行compile钩子上的方法
    this.hooks.compile.call(params);
    // 创建一个新的compilation
    const compilation = this.newCompilation(params);

    // 执行make钩子上的方法
    this.hooks.make.callAsync(compilation, err => {
      if (err) return callback(err);

      // 若compilation的finish阶段抛出错误,调用callback处理错误
      compilation.finish(err => {
        if (err) return callback(err);
                // 若compilation的seal阶段抛出错误,调用callback处理错误
        compilation.seal(err => {
          if (err) return callback(err);

                    // seal完成即编译过程完成
          // 执行afterCompile钩子上的方法,传入本次的compilation
          this.hooks.afterCompile.callAsync(compilation, err => {
            if (err) return callback(err);

            return callback(null, compilation);
          });
        });
      });
    });
  });
}

Compiler.run(callback) -> onCompiled

onCompiled是在Compiler.run中定义的,传给Compiler.compile的回调函数。在compile过程后调用,主要用于输出构建资源。

// Compiler.run(callback) -> onCompiled

const onCompiled = (err, compilation) => {
  // finalCallback前面定义的运行结束时回调
  if (err) return finalCallback(err);

  // 执行shouldEmit钩子上的方法,若返回false则不输出构建资源
  if (this.hooks.shouldEmit.call(compilation) === false) {
    // stats包含了本次构建过程中的一些数据信息
    const stats = new Stats(compilation);
    stats.startTime = startTime;
    stats.endTime = Date.now();
    // 执行done钩子上的方法,并传入stats
    this.hooks.done.callAsync(stats, err => {
      if (err) return finalCallback(err);
      return finalCallback(null, stats);
    });
    return;
  }

  // 调用Compiler.emitAssets输出资源
  this.emitAssets(compilation, err => {
    if (err) return finalCallback(err);
        // 判断资产在emit后是否需要进一步处理
    if (compilation.hooks.needAdditionalPass.call()) {
      compilation.needAdditionalPass = true;

      const stats = new Stats(compilation);
      stats.startTime = startTime;
      stats.endTime = Date.now();
      // 执行done钩子上的方法
      this.hooks.done.callAsync(stats, err => {
        if (err) return finalCallback(err);
                // 执行additionalPass钩子上的方法
        this.hooks.additionalPass.callAsync(err => {
          if (err) return finalCallback(err);
          // 再次compile
          this.compile(onCompiled);
        });
      });
      return;
    }

    // 输出records
    this.emitRecords(err => {
      if (err) return finalCallback(err);

      const stats = new Stats(compilation);
      stats.startTime = startTime;
      stats.endTime = Date.now();
      // 执行done钩子上的方法
      this.hooks.done.callAsync(stats, err => {
        if (err) return finalCallback(err);
        return finalCallback(null, stats);
      });
    });
  });
};

Compiler.emitAssets(compilation, callback)

emitAssets负责的是构建资源输出的过程,其中emitFiles是具体输出文件的方法。

// Compiler.emitAssets(compilation, callback)

emitAssets(compilation, callback) {
  let outputPath;
  // 输出打包结果文件的方法
  const emitFiles = err => {
    // ...
  };

  // 执行emit钩子上的方法
  this.hooks.emit.callAsync(compilation, err => {
    if (err) return callback(err);
    // 获取资源输出的路径
    outputPath = compilation.getPath(this.outputPath);
    // 递归创建输出目录,并输出资源
    this.outputFileSystem.mkdirp(outputPath, emitFiles);
  });
}
// Compiler.emitAssets(compilation, callback) -> emitFiles

const emitFiles = err => {
  if (err) return callback(err);

  // 异步的forEach方法
  asyncLib.forEachLimit(
    compilation.getAssets(),
    15, // 最多同时执行15个异步任务
    ({ name: file, source }, callback) => {
      // 
      let targetFile = file;
      const queryStringIdx = targetFile.indexOf("?");
      if (queryStringIdx >= 0) {
        targetFile = targetFile.substr(0, queryStringIdx);
      }
            // 执行写文件操作
      const writeOut = err => {
        // ...
      };
            // 若目标文件路径包含/或\,先创建文件夹再写入
      if (targetFile.match(/\/|\\/)) {
        const dir = path.dirname(targetFile);
        this.outputFileSystem.mkdirp(
          this.outputFileSystem.join(outputPath, dir),
          writeOut
        );
      } else {
        writeOut();
      }
    },
    // 遍历完成的回调函数
    err => {
      if (err) return callback(err);
            // 执行afterEmit钩子上的方法
      this.hooks.afterEmit.callAsync(compilation, err => {
        if (err) return callback(err);
        // 构建资源输出完成执行回调
        return callback();
      });
    }
  );
};
Compiler.emitAssets(compilation, callback) -> emitFiles -> writeOut

writeOut函数进行具体的写文件操作。

其中涉及到的两个内部Map:

_assetEmittingSourceCache用于记录资源在不同目标路径被写入的次数。

_assetEmittingWrittenFiles用于标记目标路径已经被写入的次数,key是targetPath。每次targetPath被文件写入,其对应的value会自增。

/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
this._assetEmittingSourceCache = new WeakMap();
/** @private @type {Map<string, number>} */
this._assetEmittingWrittenFiles = new Map();

关于futureEmitAssets配置项可参考output.futureEmitAssets,这里对基于垃圾回收做的内存优化(SizeOnlySource部分)还是比较有意思的。

// Compiler.emitAssets(compilation, callback) -> emitFiles -> writeOut

const writeOut = err => {
  if (err) return callback(err);
  // 解析出真实的目标路径
  const targetPath = this.outputFileSystem.join(
    outputPath,
    targetFile
  );
  // TODO webpack 5 remove futureEmitAssets option and make it on by default
  if (this.options.output.futureEmitAssets) {
    // check if the target file has already been written by this Compiler
    // 检查目标文件是否已经被这个Compiler写入过
    // targetFileGeneration是targetFile被写入的次数
    const targetFileGeneration = this._assetEmittingWrittenFiles.get(
      targetPath
    );

    // create an cache entry for this Source if not already existing
    // 若cacheEntry不存在,则为当前source创建一个
    let cacheEntry = this._assetEmittingSourceCache.get(source);
    if (cacheEntry === undefined) {
      cacheEntry = {
        sizeOnlySource: undefined,
        writtenTo: new Map() // 存储资源被写入的目标路径及其次数,对应this._assetEmittingWrittenFiles的格式
      };
      this._assetEmittingSourceCache.set(source, cacheEntry);
    }

    // if the target file has already been written
    // 如果目标文件已经被写入过
    if (targetFileGeneration !== undefined) {
      // check if the Source has been written to this target file
      // 检查source是否被写到了目标文件路径
      const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
      if (writtenGeneration === targetFileGeneration) {
        // if yes, we skip writing the file
        // as it's already there
        // (we assume one doesn't remove files while the Compiler is running)
        // 如果等式成立,我们跳过写入当前文件,因为它已经被写入过
        // (我们假设Compiler在running过程中文件不会被删除)
        compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
          size: cacheEntry.sizeOnlySource.size()
        });

        return callback();
      }
    }

    // TODO webpack 5: if info.immutable check if file already exists in output
    // skip emitting if it's already there

    // get the binary (Buffer) content from the Source
    // 获取source的二进制内容content
    /** @type {Buffer} */
    let content;
    if (typeof source.buffer === "function") {
      content = source.buffer();
    } else {
      const bufferOrString = source.source();
      if (Buffer.isBuffer(bufferOrString)) {
        content = bufferOrString;
      } else {
        content = Buffer.from(bufferOrString, "utf8");
      }
    }

    // Create a replacement resource which only allows to ask for size
    // This allows to GC all memory allocated by the Source
    // (expect when the Source is stored in any other cache)
    // 创建一个source的代替资源,其只有一个size方法返回size属性(sizeOnlySource)
    // 这步操作是为了让垃圾回收机制能回收由source创建的内存资源
    //
    // 这里是设置了output.futureEmitAssets = true时,assets的内存资源会被释放的原因
    cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
    compilation.updateAsset(file, cacheEntry.sizeOnlySource, {
      size: content.length
    });

    // Write the file to output file system
    // 将content写到目标路径targetPath
    this.outputFileSystem.writeFile(targetPath, content, err => {
      if (err) return callback(err);

      // information marker that the asset has been emitted
      compilation.emittedAssets.add(file);

      // cache the information that the Source has been written to that location
      // 缓存source已经被写入目标路径,写入次数自增
      const newGeneration =
            targetFileGeneration === undefined
      ? 1
      : targetFileGeneration + 1;
      // 将这个自增的值写入cacheEntry.writtenTo和this._assetEmittingWrittenFiles两个Map中
      cacheEntry.writtenTo.set(targetPath, newGeneration);
      this._assetEmittingWrittenFiles.set(targetPath, newGeneration);

      // 执行assetEmitted钩子上的方法
      this.hooks.assetEmitted.callAsync(file, content, callback);
    });
  } else { // webpack4的默认配置output.futureEmitAssets = false
    // 若资源已存在在目标路径 则跳过
    if (source.existsAt === targetPath) {
      source.emitted = false;
      return callback();
    }
    // 获取资源内容
    let content = source.source();

    if (!Buffer.isBuffer(content)) {
      content = Buffer.from(content, "utf8");
    }
        // 写入目标路径并标记
    source.existsAt = targetPath;
    source.emitted = true;
    this.outputFileSystem.writeFile(targetPath, content, err => {
      if (err) return callback(err);
      // 执行assetEmitted钩子上的方法
      this.hooks.assetEmitted.callAsync(file, content, callback);
    });
  }
};

至此,Compiler完成了启动构建到资源输出到过程。

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。