从源码到静态文件:构建管道与打包器抽象
前置知识
- ›理解插件生命周期与路由生成机制(第 2 篇)
- ›Webpack 基础知识(loader、plugin、bundle)
- ›熟悉 Node.js worker_threads
从源码到静态文件:构建管道与打包器抽象
路由生成完毕、代码文件写入 .docusaurus/ 之后,构建管道便接管了后续工作。它的任务是:为浏览器编译客户端 bundle,为渲染编译服务端 bundle,执行静态站点生成(SSG)以产出 HTML 文件,最后运行构建后校验。本文将完整追踪从 docusaurus build 到生产就绪的 build/ 目录的全过程。
在这一过程中,我们会看到一套整洁的打包器抽象,让 Docusaurus 能够在 Webpack 和 Rspack 之间自由切换;还会看到一种务实的多语言构建方案,以及一个可将页面渲染分发到 worker 线程并行处理的可配置 SSG 执行器。
多语言构建编排
build.ts#L23-L56 中的 build 命令负责编排所有已配置语言的构建。整体策略是串行执行:通过 mapAsyncSequential() 逐个构建每种语言。
flowchart TD
BUILD["docusaurus build"] --> LOCALES["getLocalesToBuild()"]
LOCALES --> L1["buildLocale('en')"]
L1 --> L2["buildLocale('fr')"]
L2 --> L3["buildLocale('ja')"]
L3 --> DONE["Build complete"]
style L1 fill:#e1f5fe
style L2 fill:#e1f5fe
style L3 fill:#e1f5fe
为什么选择串行?build.ts#L120-L131 的注释道出了原委:团队曾尝试在 worker_threads 中并行构建各语言,但遭遇了 SIGSEGV 和 SIGBUS 崩溃。改用 child_process 虽然可以运行,却引入了内存限制和日志方面的复杂性。串行方案是最务实的选择——它足够可靠,每个语言的构建都能独占全部 CPU 资源,充分利用 webpack 编译和 SSG 的处理能力。
默认语言始终优先构建(第 60-75 行)。这是为了避免一个隐蔽的 bug:默认语言通常输出到 build/ 根目录,而其他语言输出到 build/<locale>/ 子目录。如果最后才构建默认语言,已生成的本地化子目录就会被清空覆盖。
单语言构建管道
buildLocale.ts#L43-L150 中的每个语言构建分为五个步骤:
flowchart TD
BL["buildLocale()"] --> LOAD["1. loadSite()"]
LOAD --> CONFIGS["2. Create client + server configs (parallel)"]
CONFIGS --> COMPILE["3. compile() both bundles"]
COMPILE --> SSG["4. executeSSG()"]
SSG --> POST["5. postBuild() + handleBrokenLinks()"]
CONFIGS --> CLEAR["Clear output dir (parallel with config creation)"]
第 1 步:loadSite() — 如第 1-2 篇所述,这一步执行完整的服务端管道:加载配置、运行插件生命周期、生成代码。其中会设置 DOCUSAURUS_CURRENT_LOCALE 环境变量,作为站点配置翻译的临时处理方案。
第 2 步:创建打包器配置 — 客户端和服务端的 webpack 配置在第 80-100 行并行创建。值得注意的是,输出目录的清空也在这一步完成——它作为第三个并行 Promise 运行,因为它不依赖于配置,且本身耗时较长。
第 3 步:compile() — 两个 bundle 同时编译。如果使用 hash 路由模式,则只需要客户端 bundle(无需 SSG),服务端配置会被跳过。
第 4 步:executeSSG() — 加载服务端 bundle,将每个路由渲染为 HTML 文件。详细内容见下文。
第 5 步:构建后处理 — 运行 postBuild() 插件生命周期,让插件访问渲染输出。随后 handleBrokenLinks() 会将整个站点收集到的所有链接与所有锚点进行交叉校验。
提示: 有几个环境变量可用于调试时控制管道流程:
DOCUSAURUS_SKIP_BUNDLING、DOCUSAURUS_RETURN_AFTER_LOADING、DOCUSAURUS_EXIT_AFTER_LOADING和DOCUSAURUS_EXIT_AFTER_BUNDLING,可以让构建在任意阶段提前终止。这些变量定义于buildLocale.ts#L38-L41。
打包器抽象层
Docusaurus 通过统一接口同时支持 Webpack 和 Rspack。这套抽象位于 docusaurus-bundler 包中,核心函数是 currentBundler.ts#L27-L42 中的 getCurrentBundler():
export async function getCurrentBundler({siteConfig}): Promise<CurrentBundler> {
if (isRspack(siteConfig)) {
return {
name: 'rspack',
instance: (await importRspack()) as unknown as typeof webpack,
};
}
return {
name: 'webpack',
instance: webpack,
};
}
CurrentBundler 类型就是简单的 {name: 'webpack' | 'rspack', instance: typeof webpack}。由于 Rspack 以兼容 Webpack API 为目标,因此直接将其强制转换为相同类型。这样一来,所有下游代码只需使用 currentBundler.instance,无需关心当前激活的是哪个打包器。
graph TD
CB[getCurrentBundler] -->|rspackBundler: true| RS[Rspack instance]
CB -->|rspackBundler: false| WP[Webpack instance]
RS --> COMMON["CurrentBundler {name, instance}"]
WP --> COMMON
COMMON --> CSS[getCSSExtractPlugin]
COMMON --> COPY[getCopyPlugin]
COMMON --> PROG[getProgressBarPlugin]
不过,部分插件在两种打包器之间存在差异。currentBundler.ts#L57-L103 中的辅助函数 getCSSExtractPlugin、getCopyPlugin 和 getProgressBarPlugin 会返回各自对应的实现。对于 Rspack,CSS 提取使用内置的 CssExtractRspackPlugin,进度条则通过自定义类封装 rspack.ProgressPlugin,以模拟 WebpackBar 的 name/color API。
插件 webpack 配置的合并逻辑位于 configure.ts#L55-L78。每个插件 configureWebpack() 的返回值都会通过 webpack-merge 与现有配置进行深度合并,并支持可选的 mergeStrategy 参数,用于精细控制数组和对象的合并行为。
SSG:静态站点生成
ssgExecutor.ts 中的 SSG 执行器根据 future.faster.ssgWorkerThreads 的值在两种模式之间切换:
简单模式(createSimpleSSGExecutor,第 39-58 行)在当前 Node.js 进程中串行渲染所有页面。这是默认模式,对中小型站点来说已经足够。
线程池模式(createPooledSSGExecutor,第 101-176 行)使用 Tinypool 将页面渲染分发到多个 worker 线程并行处理。线程数量会动态计算:
flowchart TD
EX["executeSSG()"] --> CHECK{ssgWorkerThreads?}
CHECK -->|false| SIMPLE["Simple: single thread"]
CHECK -->|true| POOL["Pooled: Tinypool"]
POOL --> CALC["inferNumberOfThreads()"]
CALC --> |"pageCount / 100 vs cpuCount"| THREADS["min(workload, cpus)"]
THREADS -->|"== 1"| SIMPLE
THREADS -->|"> 1"| SPAWN["Spawn thread pool"]
SPAWN --> CHUNK["Chunk pages by SSGWorkerThreadTaskSize"]
CHUNK --> RENDER["Parallel rendering"]
ssgExecutor.ts#L65-L78 中的线程数推断逻辑使用了 minPagesPerCpu 阈值 100——如果站点只有 50 个页面,无论 CPU 核数多少都只会分配 1 个线程。这避免了在小型站点上白白消耗线程创建开销。如果推断出的线程数为 1,则直接回退到简单模式。
线程池还内置了内存管理机制:第 136 行的 maxMemoryLimitBeforeRecycle 支持在内存占用过高时回收并重建线程,以规避 issue #11161 中记录的 SSG 内存泄漏问题。
实际的渲染逻辑位于 serverEntry.tsx。对于每个页面,它会预加载路由数据,将 <App /> 包裹在 StaticRouter 和 HelmetProvider 中,渲染为 HTML,并收集失效链接数据(页面上所有的 <a> href 和 id 锚点)。收集到的数据会连同 HTML 一起返回,供构建后校验使用。
future.faster 性能标志
future.faster 配置项控制一组性能优化开关。各标志的默认值定义于 configValidation.ts#L76-L99:
| 标志 | 默认值 | 作用 |
|---|---|---|
swcJsLoader |
false |
使用 SWC 替代 Babel 进行 JS 转译 |
swcJsMinimizer |
false |
使用 SWC 进行 JS 压缩 |
swcHtmlMinimizer |
false |
使用 SWC 进行 HTML 压缩 |
lightningCssMinimizer |
false |
使用 Lightning CSS 进行 CSS 压缩 |
mdxCrossCompilerCache |
false |
在客户端/服务端构建之间缓存 MDX 编译结果 |
rspackBundler |
false |
使用 Rspack 替代 Webpack |
rspackPersistentCache |
false |
启用 Rspack 的持久化磁盘缓存 |
ssgWorkerThreads |
false |
在 worker 线程间并行执行 SSG |
gitEagerVcs |
false |
预先加载 VCS 元数据 |
使用快捷写法 future: {faster: true} 可以一次性启用所有标志(第 89-99 行)。此外还有用于前向兼容的 future.v4 标志集:
graph TD
FASTER["future.faster"] -->|true| ALL["All faster flags enabled"]
FASTER -->|object| PICK["Pick individual flags"]
V4["future.v4"] -->|true| ALL_V4["All v4 flags enabled"]
V4 -->|object| PICK_V4["Pick individual flags"]
V4 -->|fasterByDefault: true| DEFAULT["faster flags default to true"]
future.v4 标志集代表计划在 v4 中引入的破坏性变更,你现在就可以提前选择启用。有一个值得注意的依赖关系:ssgWorkerThreads 要求 同时启用 v4.removeLegacyPostBuildHeadAttribute,这一校验逻辑位于 configValidation.ts#L622-L637。原因在于 postBuild() 中遗留的 head 属性包含不可序列化的 Helmet 状态,无法在 worker 线程之间传递。
configValidation.ts#L570-L651 中的后处理逻辑负责协调 v4.fasterByDefault 与各 faster 标志之间的关系——如果 fasterByDefault 为 true,则所有未显式设置的 faster 标志都默认为 true。
提示: 现在就可以在配置中加入
future: {faster: true, v4: true}开始迁移到 v4。这将同时启用所有性能优化和前向兼容标志,让你在享受最快构建速度的同时,为下一个主版本做好准备。
下一步
我们已经完整追踪了构建管道的全貌:从 CLI 调用,经过多语言编排、打包器编译,到 SSG 渲染。但有一个环节还没有深入——打包器内部是如何将 Markdown 和 MDX 文件转换为 React 组件的。下一篇文章将深入探讨 MDX 处理管道,以及驱动文档、博客和页面功能的内容插件体系。