webpack5 打包流程源码剖析(2)

·  阅读 405

开篇

上一讲 webpack5 打包流程源码剖析(1) 中介绍了源码中「初始化阶段」和 「构建阶段」的流程分析。

本篇,我们重点分析后半部分「生成阶段」和「写入阶段」的具体流程。

一、生成阶段(seal)

在经过 「构建阶段」 后,我们可以在 compilation 实例上拿到 entiresmodules 以及每个 module 的源代码。

进入 seal 阶段,就是根据 entires 创建对应 chunk 文件,并将它所依赖 module 的代码拼接生成 assets 对象。

seal 意为密封的意思,是从 modulechunk 再到 assets 的转换过程。

assets 中存储了即将写入磁盘文件中的内容,它包含了入口模块和依赖模块的代码,以及像 __webpack_require____webpack_modules__ 运行时代码的拼接。

整个过程大致分为以下几步:

  1. 创建 chunk:this.addChunk(name)、new Entrypoint(options);
  2. chunk 优化:this.hooks.optimizeChunks.call(),像 SplitChunksPlugin 插件会在这里对 chunk 的生成规则进行调整;
  3. 模块转译:this.codeGeneration(),遍历 modules,将模块代码中 import、request 等转译成 __webpack_require__,存储在 compilation.codeGenerationResults 中;
  4. 运行时语法:processRuntimeRequirements(),收集 chunk 下所有 modules 使用到的 runtime 语句;
  5. 生成 assets:emitAsset,对代码进行合并拼接生成最终完整的 asset。

1.1、创建 chunkGraph、chunk

// webpack/lib/Compilation.js
seal(callback) {
  // 1、创建 chunkGraph
  const chunkGraph = new ChunkGraph(
    this.moduleGraph,
    this.outputOptions.hashFunction
  );
  this.chunkGraph = chunkGraph;
  // 2、触发 seal hook
  this.hooks.seal.call();
  while (this.hooks.optimizeDependencies.call(this.modules)) {};

  const chunkGraphInit = new Map();
  // 3、每个 entry 创建一个 chunk
  for (const [name, { dependencies, includeDependencies, options }] of this.entries) {
    const chunk = this.addChunk(name); // 创建 chunk
    const entrypoint = new Entrypoint(options); // 创建入口点
    entrypoint.setEntrypointChunk(chunk);
    this.chunkGroups.push(entrypoint);
    // 收集 module 依赖
    for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {
      entrypoint.addOrigin(null, { name }, dep.request);
      const module = this.moduleGraph.getModule(dep);
      if (module) {
        const modulesList = chunkGraphInit.get(entrypoint);
        if (modulesList === undefined) {
          chunkGraphInit.set(entrypoint, [module]);
        } else {
          modulesList.push(module);
        }
      }
    }
  }

  // 4、如果入口的配置中需要分离 runtime,则也会生成一个 runtime chunk
  for ([name, { options: { dependOn, runtime } }] of this.entries) { ... };

  // 5、如果依赖 module 中有动态模块,在这里也会为之创建 chunk
  buildChunkGraph(this, chunkGraphInit);
}
复制代码

1.2、module、chunk 应用插件优化

在这里会将 module、chunk 作为参数触发相关插件的同步执行,比如 SplitChunksPlugin 会对 chunk 根据一定规则进行优化,感兴趣的同学可以自行深入学习。

// webpack/lib/Compilation.js
seal(callback) {
  // ... 省略第一部分 chunk 创建相关

  // 优化 modules
  while (this.hooks.optimizeModules.call(this.modules)) {};
  // 优化 chunks
  while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {};
}
复制代码

不过在进入第三部分「模块转译」之前,还会触发一系列钩子,并为每个 module 创建 moduleHash 存储在 chunkGraph 中。

seal(callback) {
  // ... 省略
  this.hooks.beforeModuleHash.call();
  this.createModuleHashes();
  this.hooks.afterModuleHash.call();
  // ...
}
createModuleHashes() {
  const { chunkGraph } = this;
  for (const module of this.modules) {
    for (const runtime of chunkGraph.getModuleRuntimes(module)) {
      this._createModuleHash(module, chunkGraph, ...); // 满足条件的 module 创建 hash
    }
  }
}
_createModuleHash(module, chunkGraph) {
  const moduleHash = createHash(hashFunction);
  module.updateHash(moduleHash, { // 更新 module hash
    chunkGraph,
    runtime,
    runtimeTemplate
  });
  const moduleHashDigest = moduleHash.digest(hashDigest);
  chunkGraph.setModuleHashes( // 存储在 chunkGraph
    module,
    runtime,
    moduleHashDigest,
    moduleHashDigest.slice(0, hashDigestLength)
  );
  return moduleHashDigest; // hash 字符串
}
复制代码

1.3、模块转译 codeGeneration

在这里会生成一个 codeGenerationResults 对象存储转译后的所有代码。

每一个 module 都会为其创建一个转译工作 job,通过执行 module.codeGeneration 进行转换并存储在 codeGenerationResults 之中。

// webpack/lib/Compilation.js
seal(callback) {
  this.codeGeneration();
}

codeGeneration(callback) {
  const { chunkGraph } = this;
  this.codeGenerationResults = new CodeGenerationResults(
    this.outputOptions.hashFunction
  );
  const jobs = [];
  for (const module of this.modules) {
    const runtimes = chunkGraph.getModuleRuntimes(module);
    for (const runtime of runtimes) {
      const hash = chunkGraph.getModuleHash(module, runtime);
      jobs.push({ module, hash, runtime, runtimes: [runtime] });
    }
  }
  this._runCodeGenerationJobs(jobs, callback);
}

_runCodeGenerationJobs(jobs, callback) {
  const results = this.codeGenerationResults;
  asyncLib.eachLimit( // 异步 forEach
    jobs,
    this.options.parallelism,
    (job, callback) => {
      const { module, hash, runtime, runtimes } = job;
      this.codeGeneratedModules.add(module);
      // 关键,对 module 中的代码语法进行转换
      result = module.codeGeneration({
        chunkGraph,
        moduleGraph,
        dependencyTemplates,
        runtimeTemplate,
        runtime,
        codeGenerationResults: results,
        compilation: this
      });
      for (const runtime of runtimes) {
        results.add(module, runtime, result);
      }
    },
  );
}
复制代码

具体的 module 转换生成是在 module.codeGeneration 中通过 JavascriptGenerator 构建代码,涉及到很多修改语句的模板 Template,处理 import 语法转换为 webpack_require 等语法转换。

1.4、运行时语法 processRuntimeRequirements

在这里会对上一步「模块转译」中各个模块所涉及到的 runtime 语法如:webpack_require 收集和整合。

processRuntimeRequirements({
  chunkGraph = this.chunkGraph,
  modules = this.modules,
  chunks = this.chunks,
  codeGenerationResults = this.codeGenerationResults,
  chunkGraphEntries = this._getChunkGraphEntries()
} = {}) {
  // 1、收集 module 中的所有 runtime 语句
  for (const module of modules) {
    for (const runtime of chunkGraph.getModuleRuntimes(module)) {
      // 获取 module Generation 后所用到的 rumtime 代码语句
      const runtimeRequirements = codeGenerationResults.getRuntimeRequirements(module, runtime);
      let set;
      if (runtimeRequirements && runtimeRequirements.size > 0) {
        set = new Set(runtimeRequirements);
        this.hooks.additionalModuleRuntimeRequirements.call(module, set, context); // 触发 hook
        // 触发单个 runtime 语法相关的 hooks,如:__webpack_require__.r
        for (const r of set) {
          const hook = runtimeRequirementInModule.get(r);
          if (hook !== undefined) hook.call(module, set, context);
        }
        // 将 set 中的 runtime 信息记录在 ChunkGraphModule.runtimeRequirements 中(module)
        chunkGraph.addModuleRuntimeRequirements(module, runtime, set);
      }
    }
  }

  // 2、将 module 中的 runtime 语句整合在 chunk 之中
  for (const chunk of chunks) {
    const set = new Set();
    // 遍历 chunk 下的所有 module
    for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
      // 拿到上面存储在 ChunkGraphModule 中的 runtime 信息
      const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements(
        module,
        chunk.runtime
      );
      for (const r of runtimeRequirements) set.add(r);
    }
    this.hooks.additionalChunkRuntimeRequirements.call(chunk, set, context);
    // 将 set 中的 runtime 信息记录在 ChunkGraphChunk.runtimeRequirements 中(chunk)
    chunkGraph.addChunkRuntimeRequirements(chunk, set);
  }
}
复制代码

1.5、emitAsset

最后的工作是调用 createChunkAssets,在这里会进行「模块合并打包」。最终执行 renderMain 将模块代码和 webpack 语法代码进行拼接。

// webpack/lib/Compilation.js
seal(callback) {
  this.createChunkAssets(err => {
    this.logger.timeEnd("create chunk assets");
    if (err) {
      return finalCallback(err);
    }
    cont();
  });

  // 将 chunks 输出为 assets
  createChunkAssets(callback) {
    asyncLib.forEachLimit(
      this.chunks,
        15,
        (chunk, callback) => {
          // 执行 JavaScriptModulesPlugin.js compilation.hooks.renderManifest.tap,创建 manifest,其中有一个 render 方法
          const manifest = this.getRenderManifest({
            chunk,
            hash: this.hash,
            fullHash: this.fullHash,
            outputOptions,
            codeGenerationResults: this.codeGenerationResults,
            moduleTemplates: this.moduleTemplates,
            dependencyTemplates: this.dependencyTemplates,
            chunkGraph: this.chunkGraph,
            moduleGraph: this.moduleGraph,
            runtimeTemplate: this.runtimeTemplate
          });
          asyncLib.forEach(
            manifest,
            (fileManifest, callback) => {
              // 进行代码拼接,得到的就是最终要写入文件的 asset 内容
              source = fileManifest.render();
              const cachedSource = new CachedSource(source);
              cachedSourceMap.set(source, cachedSource);
              source = cachedSource;
              this.emitAsset(file, source, assetInfo); // --> this.assets[file] = source;
              chunk.files.add(file);
              this.hooks.chunkAsset.call(chunk, file);
          }
        )
      }
    )
  }

  const cont = () => {
    this.hooks.processAssets.callAsync(this.assets, err => {
      callback();
    })
  }
}
复制代码

至此,打包资源的生成与合并处理完成,得到了 assets 对象,最后,就是将 assets 内容写入到磁盘文件中。

二、输出阶段(emit)

compiler.emitAssets 是输入阶段的开始,在生成阶段 seal 确定好输出内容后,根据配置的 output,将文件内容写入到文件系统。

走到这里,就是对编译完成的 Assets 进行写入输出。具体分为以下几步:

  1. 调用 this.emitAssets,先执行 compiler.hooks.emit,再根据 config.output 创建输出目录,遍历 asstes 对资源文件进行写入;
  2. 执行 new Stats(compilation) 生成模块统计数据;
  3. 接着触发 compiler.hooks.done 通知打包资源写入完成;
  4. 最后,将 stats 统计数据传入并执行最初调用 run() 方法时所传入的 callback

下面来看看源码具体实现。

// webpack/lib/Compiler.js
const onCompiled = (err, compilation) => {
  // 1. 写入资源
  this.emitAssets(compilation, err => {
    // 2. 生成统计数据
    const stats = new Stats(compilation);
    // 3. 触发 hooks.done
    this.hooks.done.callAsync(stats, err => {
      // 4. 执行 compiler.run 时传入的 callback
      finalCallback(null, stats);
    });
  });
}

emitAssets(compilation, callback) {
  // 触发 hooks.emit
  this.hooks.emit.callAsync(compilation, err => {
    // 创建 build 目录。outputPath 为我们的 config.output.path
    mkdirp(this.outputFileSystem, outputPath, emitFiles);
  });
}

const emitFiles = err => {
  // 1. 首先获取 compilation 编译完成的 assets 
  const assets = compilation.getAssets();
  // 2. 遍历 assets 进行文件输出
  asyncLib.forEachLimit( // async 异步库,理解为 forEach assets 即可
    assets,
    15, // 一次运行 15 个异步
    ({ name: file, source, info }, callback) => {
      // 3. 若输出资源存在多级目录,依次创建目录,创建完成后执行 writeOut 写入
      if (targetFile.match(/\/|\\/)) {
        const fs = this.outputFileSystem;
        const dir = dirname(fs, join(fs, outputPath, targetFile));
        mkdirp(fs, dir, writeOut);
      } else {
        writeOut();
      }
    },
    err => {
      // 4. 写入完成
      this.hooks.afterEmit.callAsync(compilation, err => {
        return callback();
      });
    }
  )
}

const writeOut = err => {
  // 1. 拼接 output.path 得到一个绝对路径
  const targetPath = join(this.outputFileSystem, outputPath, targetFile);
  // 2. 判断文件是否存在,不存在则执行 processMissingFile 
  this.outputFileSystem.stat(targetPath, (err, stats) => {
    const exists = !err && stats.isFile();
    if (exists) { // build 目录下存在这个文件
      processExistingFile(stats);
    } else {
      processMissingFile();
    }
  });
}

const processMissingFile = () => {
  const getContent = () => {
    if (typeof source.buffer === "function") {
      return source.buffer();
    } else {
      const bufferOrString = source.source();
      if (Buffer.isBuffer(bufferOrString)) {
        return bufferOrString;
      } else {
        return Buffer.from(bufferOrString, "utf8");
      }
    }
  };
  // 1. 获取文件内容
  const content = getContent();
  // 2. 写入磁盘
  return doWrite(content);
};

const doWrite = content => {
  this.outputFileSystem.writeFile(targetPath, content, err => {
    compilation.emittedAssets.add(file);
    this.hooks.assetEmitted.callAsync(file);
  })
}

const finalCallback = (err, stats) => {
  this.running = false;
  if (callback !== undefined) callback(err, stats);
  this.hooks.afterDone.call(stats);
};
复制代码

最后

感谢阅读。

参考:

[万字总结] 一文吃透 Webpack 核心原理

分类:
前端
收藏成功!
已添加到「」, 点击更改