本篇是 Rspack 源码解析系列第二十六篇。上一篇讲了单个 CSS 模块的 parse/generate。本文进入
rspack_plugin_css的 plugin 层和rspack_plugin_css_chunking,看多个 CSS 模块如何被排成正确顺序、如何检测顺序冲突、又如何按大小切成独立 css chunk。
前言:CSS 的顺序是语义
JS 的 import 顺序通常只影响求值时机,但 CSS 的规则顺序直接影响样式优先级。
css
/* a.css */
.foo { color: red; }
/* b.css */
.foo { color: blue; }
如果 import './a.css'; import './b.css';,最终 .foo 是蓝色;反过来就是红色。所以 CSS 模块在 chunk 里的顺序必须严格遵循依赖顺序,这比 JS 的顺序约束严格得多。
源码入口:CssPlugin 的 render_manifest
crates/rspack_plugin_css/src/plugin/impl_plugin_for_css_plugin.rs 里的 render_manifest 是 CSS 最终产物的入口:
rust
#[plugin_hook(CompilationRenderManifest for CssPlugin)]
async fn render_manifest(
&self,
compilation: &Compilation,
chunk_ukey: &ChunkUkey,
manifest: &mut Vec<RenderManifestEntry>,
diagnostics: &mut Vec<Diagnostic>,
) -> Result<()> {
// ...
let css_import_modules = compilation.build_chunk_graph_artifact.chunk_graph
.get_chunk_modules_by_source_type(chunk_ukey, SourceType::CssImport, module_graph);
let css_modules = compilation.build_chunk_graph_artifact.chunk_graph
.get_chunk_modules_by_source_type(chunk_ukey, SourceType::Css, module_graph);
if css_import_modules.is_empty() && css_modules.is_empty() {
return Ok(());
}
// 计算 css 文件名(含 contenthash)
// 调用 render_chunk 生成最终 CSS
let (source, more_diagnostics) = compilation
.chunk_render_cache_artifact
.use_cache(compilation, chunk, &SourceType::Css, || async {
let (source, diagnostics) = self
.render_chunk(compilation, module_graph, chunk, &output_path,
css_import_modules, css_modules)
.await?;
Ok((CachedSource::new(source).boxed(), diagnostics))
})
.await?;
manifest.push(RenderManifestEntry {
source: source.boxed(),
filename: output_path,
has_filename: false,
info: asset_info,
auxiliary: false, // 注意:CSS 不是 auxiliary,是 chunk 主体
});
Ok(())
}
和第 24 篇 asset 对比:asset 的 auxiliary: true,而 CSS 是 auxiliary: false。因为 CSS 是 chunk 的「第二主体」(JS + CSS 并列),不是附属文件。
get_ordered_chunk_css_modules:排序入口
render_chunk 先调用 get_ordered_chunk_css_modules:
rust
async fn render_chunk(...) -> Result<(BoxSource, Vec<Diagnostic>)> {
let (ordered_css_modules, conflicts) =
Self::get_ordered_chunk_css_modules(chunk, compilation, css_import_modules, css_modules);
let source = Self::render_chunk_to_source(compilation, chunk, &ordered_css_modules, &hooks).await?;
// ...
}
get_ordered_chunk_css_modules 分成两类:css_import_modules(被 @import 引用的)和 css_modules(普通 CSS),分别排序后拼接:
rust
pub(crate) fn get_ordered_chunk_css_modules<'a>(
chunk: &Chunk,
compilation: &Compilation,
mut css_import_modules: Vec<&'a dyn Module>,
mut css_modules: Vec<&'a dyn Module>,
) -> (Vec<&'a dyn Module>, Option<Vec<CssOrderConflicts>>) {
css_import_modules.sort_unstable_by_key(|module| module.identifier());
let (mut external_css_modules, conflicts_external) =
Self::get_modules_in_order(chunk, css_import_modules, compilation);
css_modules.sort_unstable_by_key(|module| module.identifier());
let (mut css_modules, conflicts) =
Self::get_modules_in_order(chunk, css_modules, compilation);
external_css_modules.append(&mut css_modules);
// ...
}
get_modules_in_order:核心排序算法
这个算法直接对齐 webpack 的 CssModulesPlugin.js(源码注释里有对应链接)。核心思路是「按 chunk group 的 post-order index 排序」。
先看 module 在 chunk group 里的顺序:
rust
let mut modules_by_chunk_group = chunk.groups().iter()
.map(|group| compilation.build_chunk_graph_artifact.chunk_group_by_ukey.expect_get(group))
.map(|chunk_group| {
let mut indexed_modules = modules_list.clone().into_iter()
.filter_map(|module| {
chunk_group.module_post_order_index(&module.identifier())
.map(|index| (index, module))
})
.collect::<Vec<_>>();
// post order 倒序,方便用 pop() 取
let sorted_modules = {
indexed_modules.sort_by_key(|item| Reverse(item.0));
indexed_modules.into_iter().map(|item| item.1).collect::<Vec<_>>()
};
SortedModules { set: ..., list: sorted_modules }
})
.collect::<Vec<_>>();
注释解释了 post-order 的含义:
text
// ->: import
// For A -> B -> C, the pre order is A: 0, B: 1, C: 2.
// The post order is A: 2, B: 1, C: 0.
post-order 保证依赖(被 import 的)排在引用它的模块之前------这正是 CSS 需要的顺序。
多 chunk group 的冲突检测
单个 chunk group 直接返回排序结果。多个 chunk group 时,就需要解决「同一个模块在不同 group 里顺序不同」的冲突:
rust
loop {
let mut failed_modules: IdentifierSet = Default::default();
let list = modules_by_chunk_group[0].list.clone();
if list.is_empty() { break; }
let mut selected_module = *list.last().expect("TODO:");
let mut has_failed = None;
'outer: loop {
for SortedModules { set, list } in &modules_by_chunk_group {
if list.is_empty() { continue; }
let last_module = *list.last().expect("TODO:");
if last_module.identifier() == selected_module.identifier() { continue; }
if !set.contains(&selected_module.identifier()) { continue; }
failed_modules.insert(selected_module.identifier());
if failed_modules.contains(&last_module.identifier()) {
// 出现冲突,换一个候选
has_failed = Some(last_module);
continue;
}
selected_module = last_module;
has_failed = None;
continue 'outer;
}
break;
}
if let Some(has_failed) = has_failed {
// 不可解的冲突,记录并告警
let conflict = CssOrderConflicts {
chunk: chunk.ukey(),
failed_module: has_failed.identifier(),
selected_module: selected_module.identifier(),
};
// ...
}
final_modules.push(selected_module);
// 从所有 list 里移除 selected_module
// ...
}
这个算法本质是「在多个约束之间找一条拓扑排序路径」,找不到就记录 CssOrderConflicts。
冲突如何变成诊断
冲突不会中断构建,而是变成 warning:
rust
if let Some(conflicts) = conflicts {
diagnostics.extend(conflicts.into_iter().map(|conflict| {
// ...
let mut diagnostic = Diagnostic::warn(
"Conflicting order".into(),
format!(
"chunk {}\nConflicting order between {} and {}",
chunk.name().unwrap_or(...),
failed_module.readable_identifier(&compilation.options.context),
selected_module.readable_identifier(&compilation.options.context)
),
);
diagnostic.file = Some(output_path.to_owned().into());
diagnostic.chunk = Some(chunk.ukey().as_u32());
diagnostic
}));
}
render_chunk_to_source:包裹 @media/@supports/@layer
排序完成后,每个 CSS 模块需要根据它来自的 @import 上下文包裹:
rust
let mut num_close_bracket = 0;
if let Some(media) = data.get::<CssMedia>() {
num_close_bracket += 1;
container_source.add(RawStringSource::from(format!("@media {media}{{\n")));
}
if let Some(supports) = data.get::<CssSupports>() {
num_close_bracket += 1;
container_source.add(RawStringSource::from(format!("@supports ({supports}) {{\n")));
}
if let Some(layer) = data.get::<CssLayer>() {
num_close_bracket += 1;
container_source.add(RawStringSource::from(format!(
"@layer{} {{\n",
if let CssLayer::Named(layer) = &layer { Cow::Owned(format!(" {layer}")) }
else { Cow::Borrowed("") }
)));
}
container_source.add(cur_source.clone());
for _ in 0..num_close_bracket {
container_source.add(RawStringSource::from_static("\n}"));
}
如果 @import "./x.css" layer(foo) media(screen) supports(display:grid),那 x.css 的内容就被 @media + @supports + @layer 三层包裹。这些信息由第 25 篇的 CssImportDependency 的 media/supports/layer 字段带过来,存在 CodeGenerationData 里。
AUTO_PUBLIC_PATH_PLACEHOLDER 的替换
CSS 里的 url() 可能用了 publicPath: 'auto',生成时先写占位符,渲染时替换:
rust
let content = source.source().into_string_lossy();
let auto_public_path_matches: Vec<_> = content
.match_indices(AUTO_PUBLIC_PATH_PLACEHOLDER)
.map(|(index, _)| (index, index + AUTO_PUBLIC_PATH_PLACEHOLDER.len()))
.collect();
if !auto_public_path_matches.is_empty() {
let mut replace = ReplaceSource::new(source);
for (start, end) in auto_public_path_matches {
let relative = PublicPath::render_auto_public_path(compilation, output_path);
replace.replace(start as u32, end as u32, &relative, None);
}
replace.boxed()
}
和第 24 篇 asset 的 AUTO_PUBLIC_PATH_PLACEHOLDER 是同一个机制。
CssChunkingPlugin:把 CSS 拆成独立 chunk
crates/rspack_plugin_css_chunking/src/lib.rs 在 optimize_chunks 阶段(stage = 5)把 CSS 从 JS chunk 里拆出来:
rust
#[plugin_hook(CompilationOptimizeChunks for CssChunkingPlugin, stage = 5)]
async fn optimize_chunks(&self, compilation: &mut Compilation) -> Result<Option<bool>> {
// ...
const MIN_CSS_CHUNK_SIZE: f64 = 30_f64 * 1024_f64;
const MAX_CSS_CHUNK_SIZE: f64 = 100_f64 * 1024_f64;
}
默认按 30KB~100KB 拆分:小于 min 的合并,大于 max 的拆开。
strict 与 loose 两种模式
CssChunkingPluginOptions 有 strict 开关:
rust
if !self.strict {
// loose 模式:根据顺序猜测依赖关系
for b in &remaining_modules {
let mut dependents = HashSet::new();
'outer: for a in &remaining_modules {
// 检查 a 是否依赖 b
// 如果 a 依赖 b,b 必须出现在 a 之前
}
all_dependents.insert(*b, dependents);
}
}
strict 模式严格校验每个 chunk 里的顺序不被破坏;loose 模式只根据「依赖者一定排在被依赖者之前」这个启发式猜测依赖。注释说明了 loose 的假设:
text
// In loose mode we guess the dependents of modules from the order
// assuming that when a module is a dependency of another module
// it will always appear before it in every chunk.
贪心选择下一个模块
拆分时维护一个 potential_next_modules 池,贪心选择「减少请求数最多」的模块:
rust
ordered_potential_next_modules.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
'outer: for (next_module_identifier, size, _) in ordered_potential_next_modules {
if current_size + size > self.max_size {
continue; // chunk 太大
}
// strict 模式校验顺序
// loose 模式校验依赖
// 全局 CSS 校验不泄漏到无关 chunk
// ...
}
global CSS 的隔离
有一类特殊约束:全局 CSS(非 .module.css)不能泄漏进不相关的 chunk:
rust
fn is_global_css(name_for_condition: &Option<Box<str>>) -> bool {
name_for_condition.as_ref().is_some_and(|s| {
!s.ends_with(".module.css") && !s.ends_with(".module.scss") && !s.ends_with(".module.sass")
})
}
拆分时:
rust
if is_global && global_css_mode && all_chunk_states.len() != next_chunk_states.len() {
continue; // chunk group 必须完全一致
}
全局 CSS 只能和「覆盖相同 chunk group 集合」的模块分在一起,否则会把全局样式泄漏进只 import 了局部模块的 chunk。
最后:disconnect 旧 chunk,connect 新 chunk
拆完后,把 CSS 模块从原 chunk 断开,连到新的 css chunk:
rust
for chunk_state in chunk_states.values() {
let mut chunks: UkeySet<ChunkUkey> = UkeySet::default();
for module_identifier in &chunk_state.modules {
if let Some(new_chunk_ukey) = new_chunks_by_module.get(module_identifier) {
chunk_graph.disconnect_chunk_and_module(&chunk_state.chunk, *module_identifier);
// ...
chunk.split(new_chunk, &mut ...chunk_group_by_ukey);
}
}
}
Rust 角度:顺序约束是一等公民
CSS 插件最值得学习的是它把「顺序」从隐式约定变成显式算法:
text
post-order index 编码依赖顺序
多 chunk group 冲突检测
strict/loose 两种策略
global CSS 隔离约束
这些都是 webpack 多年来踩过的坑,Rspack 用 Rust 重新实现,并保留了与 webpack 对齐的行为(源码注释里多处引用 webpack 对应行号)。
这一篇应该带走什么
- CSS 是 chunk 的第二主体(
auxiliary: false),不是附属文件; get_modules_in_order用 post-order index 保证依赖在前、引用在后;- 多 chunk group 冲突通过贪心拓扑排序解决,不可解时产生
Conflicting orderwarning; render_chunk_to_source用@media/@supports/@layer包裹来自@import的模块;CssChunkingPlugin按 30KB~100KB 贪心拆分,严格保证 global CSS 隔离。
写在最后
CSS 是「顺序敏感」的模块类型。接下来换一个「异步编译」的模块类型------WebAssembly。它不读文本、不 parse AST,而是用 wasmparser 解析二进制,并把实例化推迟到运行时。下一篇进入 rspack_plugin_wasm,看 wasm 如何成为异步模块。