Rspack 源码解析 (1) —— 架构总览:从 Node.js 到 Rust 的跨界之旅

24 阅读5分钟

写在前面:本系列文章旨在通过阅读 Rspack 源码,学习rust相关使用场景,了解Rust生态中比较优秀的项目是如何管理Rust代码的,也为自己之后学习并应用Rust指明方向,也愿您能有所得。

Rspack源码结构概览

Rspack的源码是一个标准的 Monorepo单体仓库-将多个相关项目、模块的源码都放在同一个代码仓库中统一管理,而不是每个项目一个独立仓库),Rspack的源码目录下有:

  • crates: 所有Rust子模块(核心、插件、绑定层等等)
  • packages: 所有的JS/TS子包(API、CLI等)
  • tests: 自测代码
  • examples: 相关示例
  • website: 相关文档等
  • scripts: 相关构建脚本

整个项目的相对核心的目录我们已经列出来了,当然还会有一些相关配置文件没有一一列举,在后面的源码解析的整个流程中,我们会慢慢说明。

宏观架构:三层世界 Node + NAPI + Rust

基于我们上面的目录结构,可以看出来 Rspack 的整个架构分为三层,分别是:Node.js层、Binding层(Node-API)、Rust Core层

Node.js层:用户接口与生态相融

  • 职责

    • 负责与用户交互(配置、插件、Loader、Cli)
    • 保持与Webpack生态的兼容性
    • 提供JS/TS API和命令行工具
  • 代表目录/文件

    • rspack 核心 JS SDK,也就是我们安装的 @rspack/core
    • rspack-cli 命令行工具,处理 rspack build 命令
    • rspack.config.js 用户配置

Binding层:NAPI跨语言桥梁

  • 职责

    • 通过 napi-rs 将 Rust 能力暴露给 Node.js
    • 负责类型转换、内存管理、回调注册
    • 让JS插件、Loader能与Rust编译器协作
  • 代表目录/文件

    • rspack_binding_api 胶水层,定义了 Rust 如何暴露给 Node.js。
    • struct jsCompiler 、 #[napi]宏

Rust Core层:高性能编译引擎

  • 职责

    • 实现所有核心编译流程(模块解析、依赖图、代码生成、优化、产物输出)
    • 插件系统、Loader 调度、缓存、HMR、增量构建等
    • 充分利用 Rust 的并发和类型安全
  • 代表目录/文件

    • rspack_core 核心编译器,实现了 Compiler, Compilation, Plugin System 等。
    • crates/rspack_plugin_* 内置插件
    • crates/rspack_loader_* 内置Loader

源码追踪:一次构建的完整旅程

让我们随着代码的执行顺序,看看 Rspack 是如何启动的。

第一站:用户入口 (Node.js)

当你运行 rspack 时,代码最终会进入 @rspack/core 的入口。

文件:packages/rspack/src/rspack.ts

// 简化代码
export function rspack(options: RspackOptions, callback?: Callback): Compiler {
  // 1. 标准化用户配置
  const createCompiler = (userOptions: RspackOptions) => {
      const options = getNormalizedRspackOptions(userOptions);
      // 2. 创建 JS 侧的 Compiler 实例
      const compiler = new Compiler(options.context, options);
      
      // 3. 注册用户配置的插件
      if (Array.isArray(options.plugins)) {
          for (const plugin of options.plugins) {
              plugin.apply(compiler);
          }
      }
      return compiler;
  };
  
  // ...
  return compiler;
}

这部分非常容易理解,和 Webpack 几乎一模一样。

第二站:JS Compiler 与 惰性初始化

Rspack 的一个巧妙设计是 Lazy Initialization (惰性初始化)。当你 new Compiler() 时,Rust 核心其实还没启动,直到你真正调用 .run().watch() 时。

文件:packages/rspack/src/Compiler.ts

export class Compiler {
  // 持有 Rust 实例的引用
  #instance?: binding.JsCompiler; 

  constructor(context: string, options: RspackOptionsNormalized) {
    this.hooks = { ... }; // 初始化 Tapable 钩子
    // 注意:构造函数里并没有初始化 Rust 实例
  }

  // 私有方法:获取或创建 Rust 实例
  #getInstance(callback) {
    // 1. 加载 Native 绑定
    const instanceBinding = require('@rspack/binding'); 

    // 2. 调用 Rust 的构造函数
    this.#instance = new instanceBinding.JsCompiler(
      this.compilerPath,
      rawOptions, // 传入处理好的配置
      this.#builtinPlugins, // 传入内置插件
      this.#registers, // 传入 JS 回调函数的注册表(用于跨语言 Hook)
      // ... 传入文件系统
    );
  }
  
  run(callback) {
      // 真正编译时,才初始化 Rust 实例
      this.#getInstance((err, instance) => {
          instance.build(callback); // 调用 Rust 的 build
      });
  }
}

初学者提示:这里 require('@rspack/binding') 加载的是一个 .node 文件(二进制动态链接库),它是由 Rust 编译出来的。

第三站:穿越 NAPI 桥梁 (The Bridge)

现在我们进入了 crates/rspack_binding_api。这是连接 JS 和 Rust 的桥梁。

文件:crates/rspack_binding_api/src/lib.rs

Rspack 使用了 napi-rs 这个库,通过 #[napi] 宏,可以轻松地把 Rust 结构体变成 JS 类。

// 这里的 #[napi] 宏表示这个结构体会被导出给 JS 使用
#[napi(custom_finalize)] 
struct JsCompiler {
  // 内部持有一个真正的 Rust Compiler
  compiler: ManuallyDrop<Compiler>, 
}

#[napi]
impl JsCompiler {
  // 这个构造函数对应 JS 里的 new instanceBinding.JsCompiler(...)
  #[napi(constructor)]
  pub fn new(
    env: Env, // NAPI 环境上下文
    mut options: RawOptions, // 从 JS 传来的配置对象
    // ... 其他参数
  ) -> Result<Self> {
    
    // 1. 将 JS 的 RawOptions 转换为 Rust 的 CompilerOptions
    let compiler_options: rspack_core::CompilerOptions = options.try_into()?;

    // 2. 创建真正的核心编译器
    let rspack = rspack_core::Compiler::new(
        compiler_options,
        // ...
    );

    // 3. 返回包装后的 JS 对象
    Ok(Self {
      compiler: ManuallyDrop::new(Compiler::from(rspack)),
      // ...
    })
  }

  // 对应 JS 里的 instance.build()
  #[napi]
  pub fn build(&mut self, reference: Reference<JsCompiler>, f: Function) -> Result<()> {
      // 在 Rust 的异步运行时中执行构建
      self.run(...) 
  }
}

初学者提示

  • struct 类似于面向对象里的 class 属性定义。
  • impl 类似于 class 的方法定义。
  • #[napi] 是“魔法”,自动生成胶水代码,让 JS 能调用这些 Rust 代码。

第四站:核心引擎 (Rust Core)

最后,我们来到了真正干活的地方:crates/rspack_core

文件:crates/rspack_core/src/compiler/mod.rs (核心逻辑)

pub struct Compiler {
  pub options: Arc<CompilerOptions>, // 编译配置
  pub compilation: Compilation,      // 编译状态管理
  pub plugin_driver: SharedPluginDriver, // 插件驱动器
  pub loader_resolver: Arc<Resolver>, // Loader 解析器
  // ...
}

impl Compiler {
    pub fn new(...) -> Self {
        // 初始化各种核心组件
    }
}

在 Rust 侧,Compiler 是一个长期存在的对象(单例模式),它负责创建 Compilation。每次构建(Build)都会产生一个新的 Compilation,它包含了模块图(Module Graph)和 Chunk 图。

总结

通过第一篇的架构概览,我们理清了 Rspack 的启动流程:

  1. 用户在 CLI 或脚本中调用 rspack()
  2. JS 层 (packages/rspack) 处理配置,初始化 Compiler.ts
  3. Binding 层 (crates/rspack_binding_api) 利用 NAPI 接收配置,创建 Rust 实例。
  4. Core 层 (crates/rspack_core) 启动,随时准备进行编译。

给 Rust 初学者的建议: 在阅读 Rspack 源码时,不必纠结于通过 Arc, Mutex, RwLock 这种复杂的并发控制细节(虽然它们在 Rspack 中无处不在)。先关注 struct数据结构设计impl方法流程,把 Rust 当作带类型的 Python 或 C++ 来看,会更容易上手。

下一篇预告: 我们将深入 Compilation(编译过程),看看 Rspack 是如何从一个入口文件开始,构建出整个项目的依赖图谱的(Make Phase)。