Rspack 源码解析(五):CodeGenerationPass 与模块代码生成

本篇是 Rspack 源码解析系列第五篇。上一篇我们从 ModuleGraph 得到了 ChunkGraph,已经知道 index.tsmath.tslazy.ts 各自会被放入哪些 Chunk。但"放进 Chunk"不代表它们已经是浏览器可以执行的源码。本篇进入 CodeGenerationPass,看 Rspack 怎样让每个模块针对不同 runtime 生成代码、运行时依赖和 source map 等结果。

前言

先把几个阶段的输出区分开:

text 复制代码
Build Module Graph
  输出:ModuleGraph
  解决:谁依赖谁

Build Chunk Graph
  输出:ChunkGraph
  解决:谁进入哪个 Chunk,异步 Chunk 如何连接

Code Generation
  输出:CodeGenerationResults
  解决:每个 Module 在某个 runtime 下具体生成什么代码

Create Chunk Assets
  输出:Asset(main.js、lazy.js 等)
  解决:如何把一个 Chunk 中的多个模块代码拼成最终文件

本篇只聚焦第三层。CodeGenerationPass 并不直接写出 dist/main.js,而是先为模块生成"可被渲染的代码片段和元数据"。真正将这些片段拼成 asset 的工作在后面的 CreateChunkAssetsPass

CodeGenerationPass 在流水线中的位置

run_passes.rs 中,代码生成位于一系列优化和标识分配之后:

text 复制代码
BuildChunkGraphPass
  -> OptimizeModulesPass
  -> OptimizeChunksPass
  -> OptimizeTreePass
  -> OptimizeChunkModulesPass
  -> ModuleIdsPass
  -> ChunkIdsPass
  -> AssignRuntimeIdsPass
  -> OptimizeCodeGenerationPass
  -> CreateModuleHashesPass
  -> CodeGenerationPass
  -> RuntimeRequirementsPass
  -> CreateHashPass
  -> CreateModuleAssetsPass
  -> CreateChunkAssetsPass

这个顺序不是偶然的。

  • 代码生成需要知道模块在哪些 Chunk/runtime 中执行;
  • 模块 ID、Chunk ID 会直接进入生成代码;
  • Tree Shaking、模块拼接等优化必须先确定,否则会为最终不需要的模块生成无用代码;
  • 模块 hash 在代码生成前创建,用于结果缓存和内容 hash 计算。

换句话说,CodeGenerationPass 是"图优化结果"到"源代码产物"的边界。

先建立四个概念

概念 可以理解成 作用
Module::code_generation() 各类模块自己的代码生成方法 NormalModuleExternalModuleContextModule 实现不同逻辑
ModuleCodeGenerationContext 本次生成代码可读取的编译上下文 提供 Compilation、runtime、模板和模块拼接 scope
CodeGenerationResult 单个模块的生成结果 保存不同 SourceType 的 source、runtime requirements、hash 等
CodeGenerationResults 所有模块、所有 runtime 的结果仓库 供后续渲染 Chunk 使用

这里的关键点是:同一个 Module 可能在多个 runtime 下有不同生成结果。

例如 index.ts 被两个不同 entry 引用,或某个模块在不同 runtime 中有不同的可用导出、模块 ID 或 runtime requirement。Rspack 不能只用 ModuleIdentifier -> Source 的简单映射,而要按 runtime 保存结果。

Pass 入口:增量构建先缩小范围

入口位于 crates/rspack_core/src/compilation/code_generation/mod.rs

rust 复制代码
// code_generation/mod.rs(简化)
async fn code_generation_pass_impl(compilation: &mut Compilation) -> Result<()> {
  let code_generation_modules = if let Some(mutations) = compilation
    .incremental
    .mutations_read(IncrementalPasses::MODULES_CODEGEN)
    && !compilation.code_generation_results.is_empty()
  {
    // 删除已移除模块的旧结果
    // 根据 ModuleSetHashes mutation 收集受影响模块
    // 只重生成这些模块
    modules
  } else {
    *compilation.code_generation_results = Default::default();
    compilation.get_module_graph().modules_keys().copied().collect()
  };

  compilation.code_generation(code_generation_modules).await?;

  compilation.plugin_driver
    .compilation_hooks
    .after_code_generation
    .call(compilation, &mut diagnostics)
    .await?;
  Ok(())
}

首次构建时,所有模块都会进入 code generation。增量构建时,Rspack 利用前面阶段记录的 Mutation::ModuleSetHashes 缩小范围:模块没有变化、生成 hash 没有失效,就无需重新生成。

这与上一篇的 Cutout + repair 是同一种思路:整个编译器不是"文件一变就重做所有阶段",而是让每个 Artifact 都有自己的变更边界。

为什么要按 runtime 分组

Compilation::code_generation() 并不会立即遍历所有模块调用生成函数。它先区分有无 code generation dependencies 的模块,再交给 code_generation_modules()

rust 复制代码
// code_generation/mod.rs(简化)
for runtime in chunk_graph.get_module_runtimes_iter(
  module,
  &self.build_chunk_graph_artifact.chunk_by_ukey,
) {
  let hash = ChunkGraph::get_module_hash(self, module, runtime)?;

  if let Some(job) = map.get_mut(hash) {
    job.runtimes.push(runtime.clone());
  } else {
    map.insert(hash.clone(), CodeGenerationJob {
      module,
      hash,
      runtime: runtime.clone(),
      runtimes: vec![runtime.clone()],
      scope,
    });
  }
}

这段代码有两层去重。

第一层:模块按其运行时遍历

ChunkGraph::get_module_runtimes_iter() 从 ChunkGraph 中取出模块所在 ChunkGroup 的 runtime。一个模块被多个入口共享时,可能对应多个 runtime。

第二层:生成 hash 相同的 runtime 合并成一个 Job

如果多个 runtime 下该模块的生成 hash 相同,Rspack 只生成一次,再将结果复用到多个 runtime:

text 复制代码
module A
  runtime X -> hash h1
  runtime Y -> hash h1
  runtime Z -> hash h2

最终 Job:
  Job(A, h1, [X, Y])
  Job(A, h2, [Z])

这比"每个 Chunk 都重新生成其中每个模块"节省得多。它也说明代码生成结果的缓存粒度不是单纯的文件,而是"模块在等价运行时上下文中的代码"。

并行执行:先组织 Job,再并发生成

所有 CodeGenerationJob 收集完后,Rspack 使用 rspack_futures::scope 并发执行:

rust 复制代码
// code_generation/mod.rs(简化)
let results = rspack_futures::scope::<_, _>(|token| {
  jobs.into_iter().for_each(|job| {
    let s = unsafe { token.used((&self, &module_graph, job)) };

    s.spawn(|(this, module_graph, job)| async {
      let module = module_graph
        .module_by_identifier(&job.module)
        .expect("should have module");

      module.code_generation(&mut code_generation_context).await
    })
  })
})
.await;

和第三篇 Build Module Graph 的后台任务设计形成对照:

  • 模块图构建时,共享图结构会被串行写入,因此需要把 AddTask 等操作放回主任务队列;
  • 代码生成时,输入的 ModuleGraph、ChunkGraph 和前序优化结果已经稳定,每个 Job 产出独立的 CodeGenerationResult,因此适合并行计算。

这体现了一个重要的并发设计原则:不是所有编译阶段都需要同一种并发模型;能否并行取决于任务是否写同一份共享状态。

ModuleCodeGenerationContext:模块生成时能看到什么

每个 Job 会创建 ModuleCodeGenerationContext

rust 复制代码
// code_generation/mod.rs(简化)
let mut runtime_template = this.runtime_template.create_module_code_template();
let mut code_generation_context = ModuleCodeGenerationContext {
  compilation: this,
  runtime: Some(&job.runtime),
  concatenation_scope: job.scope.clone(),
  runtime_template: &mut runtime_template,
};

module.code_generation(&mut code_generation_context).await

模块生成代码时不应该自己持有整个编译器的可变状态,而是通过这个上下文读取所需能力:

字段 用途
compilation 读取 options、ModuleGraph、ChunkGraph、插件钩子等编译信息
runtime 按当前 runtime 判断依赖连接、导出名和运行时代码需求
concatenation_scope 支持 Module Concatenation 等优化后的模块组合
runtime_template 生成 __webpack_require__ 等 runtime 调用模板,并收集 runtime requirements

这是很典型的 Rust API 设计:将一次操作实际需要的上下文显式封装为参数,而不是让模块对象依赖全局状态。

NormalModule 的代码生成发生了什么

不同模块类型各自实现 Module trait 中的 code_generation()

rust 复制代码
// crates/rspack_core/src/module.rs(示意)
#[async_trait]
pub trait Module {
  async fn code_generation(
    &self,
    context: &mut ModuleCodeGenerationContext,
  ) -> Result<CodeGenerationResult>;
}

例如:

  • NormalModule:对 JavaScript/TypeScript 经 Loader、Parser 处理后的源码进行依赖替换和封装;
  • ExternalModule:生成对外部变量、CommonJS 或 ESM import 的引用代码;
  • ContextModule:生成上下文模块的查找映射;
  • ConcatenatedModule:生成 Scope Hoisting 后拼接的模块代码。

NormalModule::code_generation() 位于 crates/rspack_core/src/normal_module.rs。它不是再去读源文件或执行 Loader;这些工作已经在第三篇的 BuildTask 中完成。此时它消费已经构建好的 source 和 dependencies,并让 dependency template 根据当前 runtime 改写对应位置。

可以把这个阶段粗略理解成:

text 复制代码
Loader / Parser 之后的源码
  + 依赖信息
  + 当前 ModuleId / runtime
  + dependency templates
  -> 浏览器运行时可调用的模块源码

例如源码:

ts 复制代码
import { sum } from './math';
console.log(sum(1, 2));

经过模块化代码生成后,不再只是原始 import 文本,而会变成与模块 ID、模块工厂和 runtime loader 协作的代码形态。具体字符串会随 target、output、模块格式和优化配置而变,但原则不变:模块之间的引用从"源码层的请求"变为"运行时能解析的模块引用"。

CodeGenerationResult 包含的不只是 JavaScript

module.code_generation() 返回的不是单一字符串,而是 CodeGenerationResult。从后续渲染角度,它至少要表达:

text 复制代码
不同 SourceType 的 Source
  + runtime_requirements
  + data
  + hash

SourceType 允许模块产生不止一种类型的输出,例如 JavaScript、CSS 或 runtime 相关 source。后面的 Chunk 渲染器会按 source type 组合模块结果。

runtime_requirements 同样重要。模块本身生成代码时可能发现需要某个运行时全局能力,例如:

text 复制代码
__webpack_require__
模块缓存
异步 Chunk 加载
公开路径
ESM namespace helpers

这些 requirement 先由模块生成阶段收集,再由后面的 RuntimeRequirementsPass 汇总到 Chunk 和 Tree,最后再添加所需 runtime module。这样一个普通模块不用自己直接拼完整 runtime,只需要声明"我用到了什么"。

源码中在生成完成后会合并模板收集到的 requirement,并计算结果 hash:

rust 复制代码
// code_generation/mod.rs(简化)
module.code_generation(&mut code_generation_context)
  .await
  .map(|mut codegen_res| {
    codegen_res.runtime_requirements.extend(
      *runtime_template.runtime_requirements()
    );
    codegen_res.set_hash(
      &options.output.hash_function,
      &options.output.hash_digest,
      &options.output.hash_salt,
    );
    codegen_res
  })

缓存:模块代码生成结果如何复用

生成代码前,Rspack 会通过 code_generate_cache_artifact.use_cache() 查询缓存:

rust 复制代码
// code_generation/mod.rs(简化)
let codegen_res = this
  .code_generate_cache_artifact
  .use_cache(&job, || async {
    module.code_generation(&mut code_generation_context).await
  })
  .await;

缓存键对应 CodeGenerationJob,因此自然包含模块、hash 和 runtime 相关信息。对于未变更模块,缓存命中时就可以避免重复执行 dependency template 和源码转换。

同时,代码生成错误不会让 Rspack 在这里立刻丢弃所有其他模块结果:

rust 复制代码
let codegen_res = match codegen_res {
  Ok(codegen_res) => codegen_res,
  Err(err) => {
    let mut diagnostic = Diagnostic::from(err);
    diagnostic.module_identifier = Some(module);
    self.push_diagnostic(diagnostic);
    CodeGenerationResult::default()
  }
};

这延续了第三篇 Factorize/Build 阶段的设计:尽可能收集完整 diagnostics,最后统一向用户报告,而不是遇到第一个模块失败就停止整个编译流程。

结果如何写回 Compilation

每个并发 Job 完成后,主流程将结果写回 code_generation_results

rust 复制代码
// code_generation/mod.rs(简化)
self
  .code_generation_results
  .insert(module, codegen_res, runtimes);
self.code_generated_modules.insert(module);

可以把这个 Artifact 理解为:

text 复制代码
CodeGenerationResults
  Module(index.ts)
    Runtime(main) -> JS source + requirements + hash
  Module(math.ts)
    Runtime(main) -> JS source + requirements + hash
  Module(lazy.ts)
    Runtime(lazy) -> JS source + requirements + hash

后续阶段不会重新调用每个 Module 的 code_generation(),而是从这里读取结果来:

  • 聚合 runtime requirements;
  • 计算 compilation/chunk hash;
  • 渲染模块代码;
  • 生成 main.js、异步 Chunk 和 source map 等最终 asset。

用前三篇的示例走一遍

继续使用:

ts 复制代码
// index.ts
import { sum } from './math';
import('./lazy');
console.log(sum(1, 2));

在上一阶段,ChunkGraph 已经得到:

text 复制代码
main Chunk: index.ts, math.ts
lazy Chunk: lazy.ts

现在 CodeGenerationPass 会做:

text 复制代码
index.ts @ main runtime
  -> 生成模块代码
  -> 声明同步引用 math.ts 所需的 runtime helper
  -> 声明 import() 所需的异步 Chunk 加载 helper

math.ts @ main runtime
  -> 生成导出 sum 的模块代码

lazy.ts @ lazy runtime
  -> 生成异步 Chunk 中的模块代码

结果并不是立即写文件,而是写入 CodeGenerationResults。之后的 Chunk 渲染阶段才会将:

text 复制代码
main Chunk 的模块代码 + main 所需 runtime modules
  -> main.js

lazy Chunk 的模块代码
  -> lazy.js

这一篇应该带走什么

这一阶段的主线是:

text 复制代码
优化完成的 ModuleGraph + ChunkGraph
  -> 按 module/runtime 收集 CodeGenerationJob
  -> 相同 hash 的 runtime 合并 Job
  -> 并行调用 Module::code_generation
  -> 收集 source、runtime requirements 和 hash
  -> 写入 CodeGenerationResults
  -> 供后续 runtime 和 asset 阶段消费

从 Rust 设计角度,至少有四点值得记住:

  1. 按 runtime 而不是按文件缓存:同一模块不同执行上下文可能有不同代码;
  2. 先分组、再并行:在图结构稳定后,独立的 code generation job 可以安全并发;
  3. Capability declaration 而非直接拼 runtime :模块只收集 runtime_requirements,统一由后续阶段提供实现;
  4. Artifact 作为阶段契约CodeGenerationResults 将"生成模块代码"和"渲染最终文件"隔开,让后续 Pass 不必重新理解 Module 内部细节。

写在最后

至此,Rspack 已经完成了从源码模块到"按 runtime 生成的模块代码"的转换。距离 dist/main.js 只差最后几步:收集 runtime requirements、计算 hash、为 Chunk 创建 asset,再进入 process_assets 钩子。

下一篇可以继续追 RuntimeRequirementsPassCreateChunkAssetsPass,重点看 runtime module 如何被加入 Chunk,以及多个 CodeGenerationResult 最终怎样被渲染为浏览器能加载的文件。

相关推荐
八荒启·交互动画1 小时前
Web特效06——WebGL深挖:顶点着色器和片元着色器到底在干什么
前端·网页特效·八荒启-交互动画·八荒启
这锅我不背哈1 小时前
前端权限控制实践:基于 RBAC 的完整落地方案
前端
星空2 小时前
Map<String, String>`Map`是接口,不能直接 new
java·前端·算法
传奇开心果编程2 小时前
【xilem0.4基础语法学与练】第53课 to_do_mvc 官方示例代码深度解析
学习·rust·前端框架
开开心心就好2 小时前
低年级识字练笔顺工具,电脑和安卓端都能用
前端·javascript·网络·安全·scala·erlang·语音识别
杨利杰YJlio2 小时前
ITSK PE 26U5 测试版解读:组件完善、VMD 修复与服务器支持边界
前端·javascript·后端
志尊宝2 小时前
Vue3 零基础每日笔记(021):class 与 style 动态绑定——对象、数组、三元表达式写法汇总
前端·vue.js·笔记·前端框架·html5
夏幻灵2 小时前
Vue / React 列表渲染中的 key:为什么没有 key,Checkbox 可能“对号跑错”?
前端·javascript·vue.js
YWL2 小时前
vue-time-axis-plus进阶:双进度条+跨空白续播+播放指针,打造专业级监控回放体验
前端·javascript·vue·时间轴