Skip to content

emit阶段

seal阶段完成后,对于每一个chunk下的每一个module都已经生成了相应的代码片段。接下来就需要将这些片段进行拼接,形成可以执行的完整代码,并将代码生成到相应的文件当中。

createChunkAssets

createChunkAssets会在codeGeneration回调中调用:

javascript
asyncLib.forEachLimit(
  this.chunks,
  15,
  (chunk, callback) => {
    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
    });
    // ...
  })
)

首先会对每一个chunk创建manifest

javascript
getRenderManifest(options) {
  return this.hooks.renderManifest.call([], options);
}

compilation.hooks.renderManifest.tap(
  "JavascriptModulesPlugin",
  (result, options) => {
    // ...
    render = () =>
    this.renderMain(
      {
        hash,
        chunk,
        dependencyTemplates,
        runtimeTemplate,
        moduleGraph,
        chunkGraph,
        codeGenerationResults,
        strictMode: runtimeTemplate.isModule()
      },
      hooks,
      compilation
    );
    // ...

    result.push({
      render,
      filenameTemplate,
      pathOptions: {
        hash,
        runtime: chunk.runtime,
        chunk,
        contentHashType: "javascript"
      },
      identifier: hotUpdateChunk
      ? `hotupdatechunk${chunk.id}`
      : `chunk${chunk.id}`,
      hash: chunk.contentHash.javascript
    });

    return result;
  }
);

此时会进入到JavascriptModulesPlugin插件当中,对chunk类型进行检验,并生成带有不同render的任务,添加到result当中。完后遍历manifest,生成文件。精简后的代码如下:

javascript
asyncLib.forEach(
  manifest,
  (fileManifest, callback) => {
  // 1. 解析 chunk 的文件等信息
    if ("filename" in fileManifest) {
      file = fileManifest.filename;
      assetInfo = fileManifest.info;
    } else {
      filenameTemplate = fileManifest.filenameTemplate;
      const pathAndInfo = this.getPathWithInfo(
        filenameTemplate,
        fileManifest.pathOptions
      );
      file = pathAndInfo.path;
      assetInfo = fileManifest.info
        ? {
        ...pathAndInfo.info,
        ...fileManifest.info
      }
      : pathAndInfo.info;
    }

  // 2. 生成 chunk 代码
    source = fileManifest.render();

    // 3. 输出文件
    this.emitAsset(file, source, assetInfo);
    chunk.files.add(file);
    this.hooks.chunkAsset.call(chunk, file);

  });
)

主要流程有三部分:首先解析文件的信息,然后调用render生成代码,最后根据文件信息将生成的代码进行输出。render函数(renderMainrenderChunk)均在webpack/lib/javascript/JavascriptModulesPlugin插件中定义,其中renderMain相较于renderChunk更加复杂,因为它是作为入口文件,通常会加入一些runtime相关的执行函数等。

renderMain

最终生成的chunk代码由各个部分组成,包括我们自己写的和引用的模块代码,程序执行的一些runtime代码、程序启动代码,以及一些其他注释、立即执行等辅助结构代码。

modules代码

javascript
const chunkModules = Template.renderChunkModules(
  chunkRenderContext,
  inlinedModules
  ? allModules.filter(m => !inlinedModules.has(m))
  : allModules,
  module => this.renderModule(module, chunkRenderContext, hooks, true),
  prefix
);

调用Template.renderChunkModules函数遍历chunk中的所有module,然后将这些module形成键值对结构代码(实际上是一行一行代码组成的数组结构,但是执行时是对象结构),存放到__webpack_modules__变量当中。例如:

javascript
/******/  var __webpack_modules__ = ({

/***/ "./src/moduleA.js":
/*!************************!*\
  !*** ./src/moduleA.js ***!
  \************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"A\": () => (/* binding */ A)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nconsole.log(react__WEBPACK_IMPORTED_MODULE_0__)\n\nfunction A() {\n  console.log('==> module A');\n}\n\n//# sourceURL=webpack://study-webpack/./src/moduleA.js?");

/***/ })
/******/  });

键为module的引用路径,值为seal后的代码。

runtime代码

javascript
if (runtimeModules.length > 0) {
  source.add(
    new PrefixSource(
      prefix,
      Template.renderRuntimeModules(runtimeModules, chunkRenderContext)
    )
  );
  source.add(
  );
  // runtimeRuntimeModules calls codeGeneration
  for (const module of runtimeModules) {
    compilation.codeGeneratedModules.add(module);
  }
}

seal阶段生成代码构成中,会分析runtimeRequirements,也就是模块在转换代码时,依赖了哪些运行时的代码。比如import最后会替换成__webpack_require__函数,定义__esModule时需要__webpack_require__.r,这个时候就需要在拼接的代码中添加这些函数的定义。例如:

javascript
/******/  /* webpack/runtime/make namespace object */
/******/  (() => {
/******/   // define __esModule on exports
/******/   __webpack_require__.r = (exports) => {
/******/    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/     Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/    }
/******/    Object.defineProperty(exports, '__esModule', { value: true });
/******/   };
/******/  })();

通过这两部分就可以组成能够正常运行的完整代码了。完后通过emitFile方法记录生成的代码和对应的文件信息。

javascript
emitAsset(file, source, assetInfo = {}) {
  this.assets[file] = source;
  this._setAssetInfo(file, assetInfo, undefined);
}

_setAssetInfo(file, newInfo) {
  if (newInfo === undefined) {
    this.assetsInfo.delete(file);
  } else {
    this.assetsInfo.set(file, newInfo);
  }
}

emitAssets

回到Compiler文件中,seal阶段执行接着会执行emitAssets方法,此时会调用hooks.emit,触发emitFiles方法:

javascript
const emitFiles = err => {
  const assets = compilation.getAssets();
  compilation.assets = { ...compilation.assets };
  asyncLib.forEachLimit(
    assets,
    15,
    ({ name: file, source, info }, callback) => {}
  );
};

此时会获取compilation中需要生成文件的代码assets。然后遍历调用writeOut方法将代码写入到对应文件当中。

总结

emit阶段主要包含两个过程:

一是遍历chunk,将chunkmodules的代码拼接,对于入口文件还会拼接runtime相关代码。最后形成assetsassetsInfo对象表示文件信息和代码信息。

二是通过assetsassetsInfo对象将代码写入到对应的文件当中。