Read OSS

从源码到静态文件:构建管道与打包器抽象

高级

前置知识

  • 理解插件生命周期与路由生成机制(第 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 中并行构建各语言,但遭遇了 SIGSEGVSIGBUS 崩溃。改用 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_BUNDLINGDOCUSAURUS_RETURN_AFTER_LOADINGDOCUSAURUS_EXIT_AFTER_LOADINGDOCUSAURUS_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 中的辅助函数 getCSSExtractPlugingetCopyPlugingetProgressBarPlugin 会返回各自对应的实现。对于 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 /> 包裹在 StaticRouterHelmetProvider 中,渲染为 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 处理管道,以及驱动文档、博客和页面功能的内容插件体系。