webpack 4 源码主流程分析(十一):文件的生成

1,473 阅读2分钟

原文首发于 blog.flqin.com。如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!

资源写入文件

回到 seal。执行:

this.summarizeDependencies();

得到 this.fileDependencies, this.contextDependencies, this.missingDependencies 后,触发了一系列处理资源,优化资源的钩子之后,回到 Compiler.jscompile 里的 compilation.seal 回调。

执行:

this.hooks.afterCompile.callAsync(compilation, (err) => {
  //...
  return callback(null, compilation);
});

该钩子会触发插件 CachePlugin 相关的事件,给 compiler 的属性 _lastCompilationFileDependencies,_lastCompilationContextDependencies 分别赋值 fileDependencies,contextDependencies

创建目标文件夹

然后执行回调即 onCompiled,方法里执行:

this.emitAssets(compilation, (err) => {
  //...
});

进入 this.emitAssetsemitAssets 负责的是构建资源输出的过程。在方法里触发了 Compiler.hooks:emit,在回调里执行:

//...
outputPath = compilation.getPath(this.outputPath); // 获取资源输出的路径
this.outputFileSystem.mkdirp(outputPath, emitFiles); // 递归创建输出目录并输出资源

outputPath 为配置里的 output.path,然后调用 mkdirp 创建文件夹。

创建目标文件并写入

创建目标文件夹后,执行回调 emitFiles,在回调里通过 asyncLib.forEachLimit 并行执行对每个 file 资源文件进行路径拼接后,将每个 source 源码转换为 buffer 后(性能提升),写入真实路径的 file

asyncLib.forEachLimit(
  compilation.getAssets(),
  15,
  ({ name: file, source }, callback) => {
    //...
    const writeOut = (err) => {
      //...
      const targetPath = this.outputFileSystem.join(outputPath, targetFile); // 路径拼接,得到真实路径
      if (this.options.output.futureEmitAssets) {
        //...判断重写入 及 gc释放内存(this.assets相关重写SizeOnlySource)
      } else {
        //...
        let content = source.source(); //source为 CachedSource 实例,content为得到的资源

        if (!Buffer.isBuffer(content)) {
          content = Buffer.from(content, 'utf8'); //buffer转换,在node中提升性能
        }
        //...写入文件
        this.outputFileSystem.writeFile(targetPath, content, (err) => {
          //...
          this.hooks.assetEmitted.callAsync(file, content, callback);
        });
      }
    };
    // 若目标文件路径包含/或\,先创建文件夹再写入
    if (targetFile.match(/\/|\\/)) {
      const dir = path.dirname(targetFile);
      this.outputFileSystem.mkdirp(this.outputFileSystem.join(outputPath, dir), writeOut);
    } else {
      writeOut();
    }
  },
  // 遍历完成的回调函数
  (err) => {
    //...回调
  }
);

其中:

let content = source.source();

sourceCachedSource 实例,source.source 做了缓存判断,执行 this._source.sourcethis._sourceConcatSource 实例,该方法会遍历 children,如果子项不是字符串,则执行其 source 方法。

对于 ReplaceSource 实例来说,会执行其 _replaceString 方法,该方法里会循环处理替换在之前 资源的构建 -> 生成 chunk 资源 -> chunkTemplate -> 生成主体 chunk 代码 -> 生成每个 module 代码 push 进去的 replacements,得到替换后的字符串,合并返回 resultStr

设置 stats 并打印构建信息

所有文件都创建写入完成后,执行回调:

this.hooks.afterEmit.callAsync(compilation, (err) => {
  if (err) return callback(err);
  return callback();
});

在回调里触发 Compiler.afterEmit:hooks,在回调里执行 callbackonCompiled 里的 this.emitAssets 的回调,即执行:

//...
this.emitRecords((err) => {
  //...
  const stats = new Stats(compilation);
  stats.startTime = startTime;
  stats.endTime = Date.now();
  this.hooks.done.callAsync(stats, (err) => {
    if (err) return finalCallback(err);
    return finalCallback(null, stats);
  });
});

执行 this.emitRecords,然后在其回调里设置相关 stats,然后在 Compiler.done:hooks 的回调里执行 finalCallback,即执行文件 webpack-cli/bin/cli.js 里的 compiler.run 的回调,即 compilerCallback

方法里清除缓存之后,执行:

const statsString = stats.toString(outputOptions);
const delimiter = outputOptions.buildDelimiter ? `${outputOptions.buildDelimiter}\n` : '';
if (statsString) stdout.write(`${statsString}\n${delimiter}`);

cli 里打印出构建相关的信息。至此,构建全部结束,下一章分析打包后的文件!

本章小结

  1. 创建目标文件夹及文件并将资源写入;
  2. 写入的时候,会循环处理 source 中的 ReplaceSource 实例中的 replacements,将其替换为真实字符串;
  3. 设置 stats 并打印构建信息。