前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >webpack源码阅读之Compiler

webpack源码阅读之Compiler

作者头像
IMWeb前端团队
发布2019-12-04 16:23:11
1.1K0
发布2019-12-04 16:23:11
举报

本文作者: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完成了启动构建到资源输出到过程。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-10-12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代码特点
  • 大致流程
  • 源码阅读
    • Compiler.run(callback)
      • Compiler.readRecord(callback)
        • Compiler.compile(callback)
          • Compiler.run(callback) -> onCompiled
            • Compiler.emitAssets(compilation, callback)
              • Compiler.emitAssets(compilation, callback) -> emitFiles -> writeOut
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档