模板渲染与代码生成

151 阅读2分钟

一、MainTemplate 与 ChunkTemplate 的作用

1.1 核心职责与源码定位

在 Webpack 中,MainTemplate(源码:lib/MainTemplate.js)和 ChunkTemplate(源码:lib/ChunkTemplate.js)是控制 Bundle 输出格式的代码生成器。

  • MainTemplate 负责入口 Chunk(通常对应 entry 配置)的代码生成,包含运行时代码(Runtime)和入口模块的初始化逻辑。
  • ChunkTemplate 负责非入口 Chunk(如动态加载的异步 Chunk、通过 SplitChunksPlugin 拆分的公共代码)的代码生成,通常只包含模块定义和与 Runtime 交互的逻辑。

1.2 源码结构与关键方法

MainTemplate 核心逻辑:

// lib/MainTemplate.js
class MainTemplate extends Template {
  constructor(outputOptions) {
    super(outputOptions);
    // 注册核心 Hook
    this.hooks = {
      render: new SyncWaterfallHook(["source", "chunk", "hash", "moduleTemplate", "dependencyTemplates"]),
      // ... 其他 Hook
    };
  }

  // 渲染入口 Chunk 的核心方法
  render(hash, chunk, moduleTemplate, dependencyTemplates) {
    // 生成 Runtime 代码
    const buf = this.hooks.bootstrap.call(
      "",
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates
    );
    // 拼接模块代码
    const source = this.hooks.render.call(
      new OriginalSource(
        buf + "\n" + chunk.modules.map(m => m.source()).join("\n"),
        "webpack/bootstrap"
      ),
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates
    );
    return source;
  }
}

ChunkTemplate 核心逻辑:

// lib/ChunkTemplate.js
class ChunkTemplate extends Template {
  render(chunk, moduleTemplate, dependencyTemplates) {
    // 直接渲染模块代码,不包含 Runtime
    const source = new ConcatSource();
    source.add(`// Chunk: ${chunk.id}\n`);
    chunk.modules.forEach(module => {
      source.add(module.source(moduleTemplate, dependencyTemplates));
    });
    return source;
  }
}

1.3 实际场景对比

  • 入口 Chunk(MainTemplate):生成 bundle.js,包含 __webpack_require__、模块加载逻辑、启动代码。
  • 异步 Chunk(ChunkTemplate):生成 0.bundle.js,仅包含模块定义和 JSONP 加载代码。

二、运行时代码(webpack_require)的生成逻辑

2.1 运行时代码组成

运行时代码负责模块的加载、缓存、解析依赖,核心函数 __webpack_require__ 的实现逻辑如下:

// 伪代码简化版 Runtime
function __webpack_require__(moduleId) {
  // 1. 检查模块缓存
  if (__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }
  // 2. 创建新模块并缓存
  const module = (__webpack_module_cache__[moduleId] = { exports: {} });
  // 3. 执行模块函数
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  // 4. 返回模块导出
  return module.exports;
}

2.2 源码生成逻辑

运行时代码的生成分散在多个插件中,但核心逻辑由 RuntimePlugin(源码:lib/RuntimePlugin.js)注入到 MainTemplate。

关键步骤:

  1. 定义 RuntimeGlobals:在 lib/RuntimeGlobals.js 中声明全局变量(如 __webpack_require__)。
  2. 生成函数体:通过 RuntimeTemplate(源码:lib/RuntimeTemplate.js)生成 __webpack_require__ 的代码字符串。
  3. 注入到 MainTemplate:在 Compilation 阶段,RuntimePlugin 向 MainTemplate 的 Hook 注册代码生成逻辑。

源码片段示例:

// lib/RuntimePlugin.js
compilation.runtimeTemplate.requireFn = "__webpack_require__";

// lib/MainTemplate.js
this.hooks.bootstrap.tap("MainTemplate", (source, chunk, hash) => {
  // 生成 __webpack_require__ 定义
  const requireCode = compilation.runtimeTemplate.requireFn;
  return Template.asString([
    source,
    `var ${requireCode} = ${runtimeTemplate.basicFunction("moduleId", [
      "// Check if module is cached",
      `if (__webpack_module_cache__[moduleId]) {`,
      Template.indent(`return __webpack_module_cache__[moduleId].exports;`),
      "}",
      "// Create a new module and cache it",
      `var module = __webpack_module_cache__[moduleId] = {`,
      Template.indent("exports: {}"),
      "};",
      "// Execute the module function",
      `__webpack_modules__[moduleId](module, module.exports, ${requireCode});`,
      "// Return the exports",
      "return module.exports;"
    ])};`
  ]);
});

2.3 动态加载与代码分割

对于异步 Chunk,运行时代码还会包含动态加载逻辑(如 JSONP):

// 由 MainTemplate 生成的加载函数
__webpack_require__.e = function(chunkId) {
  return Promise.resolve().then(() => {
    const script = document.createElement('script');
    script.src = __webpack_require__.p + chunkId + ".bundle.js";
    document.head.appendChild(script);
  });
};

三、实际场景分析

场景 1:单入口打包

  • 输入entry: './src/index.js'
  • 输出main.js 包含完整的运行时代码和模块逻辑。

场景 2:动态导入

// 源码中使用 import()
import('./module.js').then(m => m.doSomething());
  • 输出main.js(MainTemplate)包含 __webpack_require__.e0.bundle.js(ChunkTemplate)包含 window["webpackChunk"].push() 代码。

场景 3:多入口

  • 输入entry: { a: './a.js', b: './b.js' }
  • 输出a.jsb.js 各自包含独立的运行时代码(除非配置 optimization.runtimeChunk 提取公共 Runtime)。

四、总结

  • MainTemplate 是入口 Chunk 的代码骨架,承载运行时的核心逻辑。
  • ChunkTemplate 是异步/分割 Chunk 的生成器,依赖主 Runtime 的加载能力。
  • 运行时代码 通过插件系统动态注入,确保模块化机制的正确运行。