本篇是 Rspack 源码解析系列第二十二篇。第十四篇讲了 NormalModuleFactory 的 factorize 链,但那时只关注「从依赖变成普通模块」。本文进入
rspack_plugin_externals,看 externals 如何利用同一个 factorize Hook,直接产出一个完全不同的ExternalModule,从而让react、lodash这类依赖不被打包。
前言:externals 是「在模块创建前拦截」
很多人以为 externals 是「打包时排除某些依赖」。更准确地说,它是在 NormalModuleFactory 把依赖变成一个真实模块之前,抢先返回一个「外部模块」。
text
遇到 import React from 'react'
-> factorize Hook 先被 ExternalsPlugin 拦截
-> 返回 ExternalModule(不读取 node_modules 源码)
-> 普通模块创建流程不再执行
ExternalModule 的 code generation 会生成「从全局变量/require 取模块」的代码,而不是「把源码包进来」。
源码入口:一个插件只 tap 一个 Hook
crates/rspack_plugin_externals/src/plugin.rs 的 apply 非常短:
rust
impl Plugin for ExternalsPlugin {
fn name(&self) -> &'static str {
"rspack.ExternalsPlugin"
}
fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
ctx
.normal_module_factory_hooks
.factorize
.tap(factorize::new(self));
Ok(())
}
}
整个插件只做一件事:在 normal_module_factory_hooks.factorize 上 tap。这个 Hook 的返回值是 Option<BoxModule>------返回 Some 就代表「这个依赖已经处理完了,别走后面的模块创建」。
ExternalsPlugin 的结构
rust
#[plugin]
#[derive(Debug)]
pub struct ExternalsPlugin {
externals: Vec<ExternalItem>,
r#type: ExternalType,
place_in_initial: bool,
}
三个字段:
externals:用户配置的 external 规则列表;type:默认 external 类型(如commonjs、var、module);place_in_initial:是否只在 initial chunk 生效。
factorize:遍历规则并命中
factorize Hook 的核心逻辑是一个 for external_item in &self.externals 循环:
rust
#[plugin_hook(NormalModuleFactoryFactorize for ExternalsPlugin, tracing=false)]
async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result<Option<BoxModule>> {
let dependency = data.dependencies[0]
.as_module_dependency()
.expect("should be module dependency");
let context = &data.context;
for external_item in &self.externals {
match external_item {
ExternalItem::Object(eh) => { /* 精确 key 匹配 */ }
ExternalItem::RegExp(r) => { /* 正则匹配 request */ }
ExternalItem::String(s) => { /* 字符串相等匹配 */ }
ExternalItem::Fn(f) => { /* 异步函数,返回是否 external */ }
}
}
Ok(None)
}
四种 ExternalItem 分别对应四种配置写法:
js
externals: {
react: 'react', // Object
/^lodash$/: 'lodash', // RegExp
jquery: 'jQuery', // String
}
// 或
externals: ({ context, request }, callback) => { ... } // Fn
命中后调用 handle_external 生成 ExternalModule,然后 return Ok(maybe_module.map(|i| i.boxed()))。
注意:函数类型的 externals 是异步的
ExternalItem::Fn 分支里调用了 .await:
rust
ExternalItem::Fn(f) => {
let request = dependency.request();
let result = f(ExternalItemFnCtx {
context: context.to_string(),
request: request.to_string(),
dependency_type: dependency.category().to_string(),
context_info: ContextInfo {
issuer: data.issuer.clone().map(|i| i.to_string()).unwrap_or_default(),
issuer_layer: data.issuer_layer.clone(),
},
resolve_options_with_dependency_type: ResolveOptionsWithDependencyType {
resolve_options: data.resolve_options.clone().map(|r| Box::new(Arc::unwrap_or_clone(r))),
resolve_to_context: false,
dependency_category: *data.dependencies.first().expect("Expected at least one dependency").category(),
},
resolver_factory: data.resolver_factory.clone(),
})
.await?;
// ...
}
ExternalItemFnCtx 提供了 context、request、issuer、resolver_factory 等信息,JS 侧的 externals 函数可以据此做任意复杂判断。
handle_external:四种配置值形态
handle_external 处理 ExternalItemValue,它也是四种:
rust
pub enum ExternalItemValue {
String(String),
Array(Vec<String>),
Bool(bool),
Object(HashMap<String, Vec<String>>),
}
对应配置:
js
externals: {
a: 'commonjs react', // String(带类型前缀)
b: ['var', 'lodash'], // Array
c: true, // Bool
d: { // Object
commonjs: 'react',
},
}
注意 parse_external_type_from_str:
rust
fn parse_external_type_from_str(v: &str) -> Option<(ExternalType, String)> {
if UNSPECIFIED_EXTERNAL_TYPE_REGEXP.is_match(v)
&& let Some((t, c)) = v.split_once(' ')
{
return Some((t.to_owned(), c.to_owned()));
}
None
}
它用正则 ^[a-z0-9-]+ 判断字符串是否带类型前缀。'commonjs react' 会被解析成 type=commonjs、request=react;'react' 则用插件默认 type。
ExternalModule 的构造
handle_external 最终调用:
rust
Some(ExternalModule::new(
external_module_config,
r#type.unwrap_or(external_module_type),
dependency.request().to_owned(),
dependency_meta,
self.place_in_initial,
))
external_module_config 是 ExternalRequest,区分 Single(单请求)和 Map(多请求映射)。dependency_meta 记录了 external_type、attributes 等信息,其中 external_type 会根据依赖类型判断:
rust
let mut dependency_meta: DependencyMeta = DependencyMeta {
attributes: dependency.get_attributes().cloned(),
external_type: {
if dependency.as_any().downcast_ref::<ImportDependency>().is_some() {
Some(ExternalTypeEnum::Import)
} else if dependency.as_any().downcast_ref::<ESMImportSideEffectDependency>().is_some() {
Some(ExternalTypeEnum::Module)
} else {
None
}
},
source_type: None,
};
import 语句的外部模块类型是 Import,带副作用的 ESM import 是 Module------这影响后续 code generation 怎么生成导入代码。
三个 target 插件
lib.rs 还导出了三个 target 相关插件:
rust
pub use electron_target_plugin::{ElectronTargetContext, electron_target_plugin};
pub use http_externals_plugin::http_externals_rspack_plugin;
pub use node_target_plugin::node_target_plugin;
node_target_plugin:target 为 node 时,自动把内置模块(fs、path 等)当 externals;electron_target_plugin:electron 的 renderer/main 环境处理;http_externals_plugin:把 HTTP(S) 请求当 externals。
它们本质上都是「在 factorize 阶段按规则返回 ExternalModule」的具体策略。
一个具体例子
js
// rspack.config.js
module.exports = {
externals: {
react: 'React',
lodash: 'commonjs lodash',
},
};
js
import React from 'react';
import _ from 'lodash';
factorize 阶段:
text
request "react"
-> 命中 Object,value = "React"
-> 默认 type,生成 ExternalModule("React")
request "lodash"
-> 命中 Object,value = "commonjs lodash"
-> 解析出 type=commonjs, request=lodash
-> 生成 ExternalModule("lodash", type=commonjs)
最终生成的代码大致是:
js
// react(默认 var 类型)
var react = React;
// lodash(commonjs 类型)
var lodash = require("lodash");
为什么 externals 这么「轻」
对比 NormalModule 的创建:解析路径、读文件、跑 loader、parse、分析依赖------整个流程非常重。而 externals 只需要:
text
匹配 request
-> 构造 ExternalModule
-> 返回
它完全不读磁盘、不 parse、不分析依赖。所以 externals 是「构建期排除依赖」最便宜的方案,也是 library 开发(把 react 交给宿主应用)的标准做法。
与 Module Federation 的呼应
第 30 篇会看到 RemoteModule。其实 externals 和 remote 是同一个思路的两种形态:用一个「假模块」代替「真实模块」,把加载逻辑推迟到运行时。
区别在于:
text
externals:运行时从全局变量或 require 取
remote:运行时从远程容器取(异步加载)
两者都在 factorize/build 阶段拦截,返回非 NormalModule 的 Module 实现。
Rust 角度:拦截点就是扩展点
ExternalsPlugin 演示了 Rspack 插件体系的核心价值:Hook 不只是「通知」,而是「可以改变控制流」。
factorize 的返回值 Option<BoxModule> 就是控制流开关:
text
Some(module) -> 跳过后续模块创建
None -> 继续走 NormalModuleFactory 的默认流程
这比「先创建模块,再在事后删除」要高效得多,也说明 Rspack 把「模块创建」设计成一个可插拔的决策点。
这一篇应该带走什么
- externals 是在 factorize 阶段拦截,返回
ExternalModule,跳过真实模块创建; ExternalsPlugin只 tapnormal_module_factory_hooks.factorize;ExternalItem有 Object/RegExp/String/Fn 四种规则,ExternalItemValue有 String/Array/Bool/Object 四种形态;- 字符串可带类型前缀(
commonjs react),由正则解析; - externals 是「构建期排除依赖」最便宜的方案,与 remote 共享同一思路。
写在最后
externals 处理的是「不打包的模块」。但还有一种更基础、更常见的资源------JSON 文件。它如何被 parse、如何参与 tree shaking(只保留用到的字段)、如何 code generate?下一篇进入 rspack_plugin_json,看 JSON 模块的完整生命周期。