本篇是 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 可以支持多种模块来源,同时保持核心流水线稳定。
这一篇应该带走什么
- Resolver 负责把 request 解析成 resource;
- NormalModuleFactory 负责把解析结果创建成 Module;
- beforeResolve、factorize、resolve、afterResolve、createModule 是重要扩展点;
- dependency type 决定同一个 request 在构建图中的语义;
- Factory 的本质是把复杂外部输入收敛成统一 Module 抽象。
写在最后
request 被解析成资源后,还要真正读取文件、执行 loader、得到转换后的源码。下一篇继续看 Loader:Rspack 如何把 Rust 的 NormalModule 构建过程与 JavaScript loader runner 连接起来。