Rspack 源码解析(十四):Resolver 与 NormalModuleFactory

本篇是 Rspack 源码解析系列第十四篇。前面我们重点看了 Compilation 后半段:图优化、代码生成、runtime、hash 和 asset。本文回到模块构建入口,追踪一次普通依赖请求如何经过 Resolver 与 NormalModuleFactory,最终变成一个可以进入 ModuleGraph 的模块。

前言:import 不是直接读文件

源码里写:

js 复制代码
import './foo';

构建器不能简单把它拼成:

text 复制代码
当前目录 + foo.js

因为解析规则还涉及:

text 复制代码
resolve.alias
resolve.extensions
package.json exports/imports
mainFields
conditionNames
symlink
fullySpecified
loader
resource query
scheme

所以 Rspack 和 webpack 一样,把这件事拆成两层。

源码入口:normal_module_factory.rs

NormalModuleFactory 的核心源码在:

text 复制代码
crates/rspack_core/src/normal_module_factory.rs

文件开头直接定义了这一组 Hook:

rust 复制代码
define_hook!(NormalModuleFactoryBeforeResolve: SeriesBail(data: &mut ModuleFactoryCreateData) -> bool);
define_hook!(NormalModuleFactoryFactorize: SeriesBail(data: &mut ModuleFactoryCreateData) -> BoxModule);
define_hook!(NormalModuleFactoryResolve: SeriesBail(data: &mut ModuleFactoryCreateData) -> NormalModuleFactoryResolveResult);
define_hook!(NormalModuleFactoryResolveForScheme: SeriesBail(data: &mut ModuleFactoryCreateData, resource_data: &mut ResourceData, for_name: &Scheme) -> bool);
define_hook!(NormalModuleFactoryAfterResolve: SeriesBail(data: &mut ModuleFactoryCreateData, create_data: &mut NormalModuleCreateData) -> bool);
define_hook!(NormalModuleFactoryCreateModule: SeriesBail(data: &mut ModuleFactoryCreateData, create_data: &mut NormalModuleCreateData) -> BoxModule);

所以第十四篇讲的不是抽象概念,而是这个文件里真实存在的一条 Hook 链。

源码中的 NormalModuleFactory 结构体只持有三类核心依赖:

rust 复制代码
pub struct NormalModuleFactory {
  options: Arc<CompilerOptions>,
  loader_resolver_factory: Arc<ResolverFactory>,
  plugin_driver: SharedPluginDriver,
}

options 提供 rules、parser、generator、resolve 配置;loader_resolver_factory 专门解析 loader;plugin_driver 调用工厂相关 Hook。

所以 Rspack 和 webpack 一样,把这件事拆成两层:

text 复制代码
Resolver:
  request -> resource

NormalModuleFactory:
  resolve data -> module create data -> Module

Resolver 解决"它在哪里",Factory 解决"它是什么模块"。

ResolverFactory:不同类型的 resolver

Rspack 中会创建不同用途的 resolver。常见类型有:

类型 用途
normal 解析普通业务模块,例如 import './a'
loader 解析 loader,例如 babel-loader
context 解析上下文依赖,例如动态目录依赖

这也是为什么配置里有:

js 复制代码
resolve: {}
resolveLoader: {}

业务模块和 loader 本身是两类对象。它们都需要解析,但规则不一定相同。

ResolverFactory 为什么要缓存

解析会在每个模块、每条依赖上发生。如果每次都重新创建 resolver,成本会很高。

因此 Rspack 会在 resolve 配置和文件系统稳定时复用 resolver factory。

可以理解成:

text 复制代码
resolve options 没变
  -> resolver factory 可复用

resolve options 或 input filesystem 变了
  -> 清空旧 resolver factory
  -> 重新创建

这和整条编译流水线中的 Artifact 思路一致:稳定输入对应稳定产物。

NormalModuleFactory 的 Hook 链

一次普通模块创建会经过多个扩展点。

源码里 ModuleFactory for NormalModuleFactory 的 create 方法先跑 before_resolve,再跑 factorize:

rust 复制代码
async fn create(&self, data: &mut ModuleFactoryCreateData) -> Result<ModuleFactoryResult> {
  if let Some(before_resolve_data) = self.before_resolve(data).await? {
    return Ok(before_resolve_data);
  }
  let mut factory_result = self.factorize(data).await?;
  ...
  Ok(factory_result)
}

factorize 内部再依次调用:

rust 复制代码
normal_module_factory_hooks.factorize.call(data).await?
normal_module_factory_hooks.resolve.call(data).await?
self.resolve_normal_module(data).await?

如果插件在 factorize 或 resolve 阶段直接返回模块,默认的普通文件解析就不会继续执行。

一次普通模块创建会经过多个扩展点:

text 复制代码
beforeResolve
  -> factorize
  -> resolve
  -> resolveForScheme
  -> afterResolve
  -> createModule

每个阶段允许插件做不同事情。

这条链路非常重要,因为很多 webpack 插件能力都发生在这里。例如 externals、alias 改写、虚拟模块、特殊协议模块,都可能通过这些 Hook 介入。

beforeResolve:最早的拦截点

beforeResolve 发生在真正文件系统解析之前。

此时插件可以:

text 复制代码
修改 request
修改 context
根据 dependency type 忽略某个依赖
提前 bail

例如某些插件会忽略 moment 的 locale,或者把某个 request 改写到另一个路径。

如果这类操作能在 resolve 前完成,就可以避免不必要的文件系统访问。

源码中的 bail 语义也很清楚:

rust 复制代码
if let Some(false) = self
  .plugin_driver
  .normal_module_factory_hooks
  .before_resolve
  .call(data)
  .await?
{
  return Ok(Some(ModuleFactoryResult::default()));
}

当 Hook 返回 Some(false),这个依赖会被忽略,后续 resolve、loader、module 创建都不会继续。

factorize:直接返回模块的机会

factorize 阶段允许插件直接产出一个模块,而不是继续走普通 NormalModule 创建流程。

典型场景是 external:

js 复制代码
import React from 'react';

如果配置要求 React 走外部全局变量,那么它可能不会被解析成 node_modules/react,而是直接创建一个 ExternalModule。

流程可以表示为:

text 复制代码
factorize
  |
  |-- 返回 Module
  |     -> 直接使用
  |
  |-- 不返回
        -> 继续 resolve

这说明 Factory 不只是"创建普通文件模块",它也是把不同模块来源统一成 Module 抽象的入口。

源码里的 factorize 也体现了 external/ignored 这类场景:

rust 复制代码
if let Some(result) = normal_module_factory_hooks.resolve.call(data).await? {
  if let NormalModuleFactoryResolveResult::Module(result) = result {
    return Ok(ModuleFactoryResult::new_with_module(result));
  } else {
    let mut raw_module = RawModule::new(
      "/* (ignored) */".to_owned(),
      module_identifier,
      format!("{} (ignored)", data.request),
      Default::default(),
    ).boxed();
    ...
    return Ok(ModuleFactoryResult::new_with_module(raw_module));
  }
}

也就是说,Hook 可以直接返回一个 BoxModule,也可以返回 ignored,然后由 Factory 创建一个 RawModule 占位。

resolve:把 request 变成 resource

resolve 阶段会结合 context 和 request,得到最终资源信息。

例如:

text 复制代码
context: /project/src
request: ./foo

可能解析成:

text 复制代码
/project/src/foo.ts

这个过程中会应用 extensions、alias、package exports、mainFields 等规则。

解析结果不仅是路径,还可能包含:

text 复制代码
query
fragment
description file data
resolve metadata

这些信息会继续影响模块类型、loader 匹配和缓存失效。

resolveForScheme:处理特殊协议

不是所有 request 都是普通文件路径。现代构建器还会遇到:

text 复制代码
data:
file:
node:
webpack:

这类 request 不应该简单交给文件系统 resolver。

resolveForScheme 给插件一个处理特殊 scheme 的机会。例如 data: URL 可以直接变成一个内联模块,而不是去磁盘查找文件。

afterResolve:创建模块前最后修改

afterResolve 阶段已经拥有比较完整的 NormalModuleCreateData。

插件可以修改:

text 复制代码
resource
loaders
module type
parser options
generator options
match resource

例如 loader 插件、rule 匹配结果、特殊模块类型设置,都可能在这里体现。

一旦通过 afterResolve,数据就会进入真正的模块创建。

createModule:生成 Module

默认情况下,createModule 会创建 NormalModule。

但 Hook 仍然允许插件接管创建过程。最终只要能生成符合接口的 Module,后续 ModuleGraph、CodeGeneration、ChunkGraph 就可以继续处理。

这就是 Rspack 内部抽象的价值:

text 复制代码
前面阶段处理复杂输入
后面阶段只依赖 Module trait

后续流程并不需要关心这个模块最初来自真实文件、虚拟文件、data URL 还是 external。

request、resource、matchResource

模块工厂里有几个容易混淆的概念:

名称 含义
request 依赖中写下的请求字符串
raw request 更接近用户原始输入的 request
resource resolver 解析后的真实资源
matchResource 用于 rule 匹配的替代资源
userRequest 面向用户展示的请求

源码中对 inline loader 的拆分也在 resolve_normal_module 里。它会识别 !、-!、!! 这些 webpack loader 前缀:

rust 复制代码
no_pre_auto_loaders = matches!(first_char, Some('-')) && matches!(second_char, Some('!'));
no_auto_loaders = no_pre_auto_loaders || matches!(first_char, Some('!'));
no_pre_post_auto_loaders = matches!(first_char, Some('!')) && matches!(second_char, Some('!'));

然后通过 split_element 把 loader 和 resource 拆开:

rust 复制代码
let mut raw_elements = split_element(s);
unresolved_resource = raw_elements.pop().ok_or_else(...)?;
inline_loaders.extend(raw_elements.into_iter().map(|r| ModuleRuleUseLoader { ... }));

例如 inline loader:

text 复制代码
babel-loader!./src/index.ts

其中 loader、resource、userRequest 都需要被拆开处理。理解这些字段,是理解 rule、loader 和模块创建逻辑的前提。

dependency type 为什么重要

同样一个 request,在不同语法中含义可能不同:

js 复制代码
import './a'
require('./a')
import('./a')
new URL('./a.png', import.meta.url)

它们对应的 dependency type 不同,后续可能影响:

text 复制代码
使用哪个 factory
是否产生异步 Chunk
使用哪个 parser/generator
需要哪些 runtime requirements

所以 NormalModuleFactory 接收到的不是孤立字符串,而是带语义的依赖请求。

和 JS 插件桥接的关系

第十三篇讲过,JS 插件的 beforeResolve、afterResolve 等 tap 会被包装成 Rust Hook。

一次 JS 插件修改 request 的过程是:

text 复制代码
Rust ModuleFactoryCreateData
  -> JsResolveData
  -> JS plugin callback
  -> JS 修改字段
  -> 写回 Rust data
  -> Rust 继续执行

这就是 webpack 生态插件能继续影响 Rspack 模块解析的关键。

从 import 到 NormalModule

完整链路可以概括为:

text 复制代码
源码 import './foo'
  |
  v
Parser 生成 Dependency
  |
  v
根据 dependency type 找到 module factory
  |
  v
beforeResolve
  |
  v
factorize
  |
  v
resolve / resolveForScheme
  |
  v
afterResolve
  |
  v
createModule
  |
  v
NormalModule
  |
  v
BuildModuleGraph 构建并解析依赖

这条线是 ModuleGraph 的入口。解析和模块创建一旦错了,后面的图优化、代码生成、asset 生成都无从谈起。

Rust 角度:Factory 把外部复杂性收敛成内部对象

文件系统、package.json、alias、loader、scheme 都属于外部世界的复杂性。

NormalModuleFactory 的职责是把这些复杂输入收敛成统一的内部对象:

text 复制代码
Box<dyn Module>

后续阶段只面向 Module 接口编程。这种设计让 Rspack 可以支持多种模块来源,同时保持核心流水线稳定。

这一篇应该带走什么

  1. Resolver 负责把 request 解析成 resource;
  2. NormalModuleFactory 负责把解析结果创建成 Module;
  3. beforeResolve、factorize、resolve、afterResolve、createModule 是重要扩展点;
  4. dependency type 决定同一个 request 在构建图中的语义;
  5. Factory 的本质是把复杂外部输入收敛成统一 Module 抽象。

写在最后

request 被解析成资源后,还要真正读取文件、执行 loader、得到转换后的源码。下一篇继续看 Loader:Rspack 如何把 Rust 的 NormalModule 构建过程与 JavaScript loader runner 连接起来。

相关推荐
Kapaseker43 分钟前
秒懂 Rust 的 7 个核心概念
rust
PC2005_cloud43 分钟前
Spring Boot 事务回滚方法
前端·后端
颜进强43 分钟前
装了个 AI Skill 却查不了数据?一篇讲透 Skill 调用接口的三种方式(scripts / CLI / MCP)
前端·后端·ai编程
RobinDevNotes43 分钟前
用 godot-rust 给 Godot 写 Rust 扩展
rust·游戏开发
无责任此方_修行中44 分钟前
插件+1:MiaoMint —— 类 RayCast 的标签管理工具
前端·javascript·vibecoding
IT_陈寒44 分钟前
Redis误删数据后的血泪教训:我竟然这样找回来了
前端·人工智能·后端
去伪存真1 小时前
从零到一开发一个英语情景教学Agent
前端·人工智能
创新技术阁1 小时前
FastapiAdmin插件介绍
前端·后端·fastapi
白雾茫茫丶1 小时前
VibeCoding 一套 Admin 系统,五种技术栈实现
前端·ai编程·vibecoding