Read OSS

Docusaurus 架构:Monorepo 全局导览

中级

前置知识

  • React 基础知识(组件、Hooks、Context)
  • 熟悉 Node.js 以及 npm/yarn workspaces
  • 对静态站点生成器有基本了解

Docusaurus 架构:Monorepo 全局导览

从 React Native 到 Jest,再到 Supabase,数以万计的文档站点都在使用 Docusaurus——但真正深入研究过其内部实现的开发者却寥寥无几。它的底层是一个由 Lerna 管理的、包含 40 个包的 Yarn workspaces monorepo,并在架构上将服务端 Node.js 调度逻辑与客户端 React 渲染逻辑清晰地分离开来。理解这一分层是读懂所有相关代码的前提。

本文将为你建立必要的心智模型。我们会梳理包的分类体系,厘清两个执行环境的边界,剖析 CLI 的运作方式,并追踪核心 loadSite() 流水线从配置加载到代码生成的全过程。读完之后,无论遇到哪种 Docusaurus 行为,你都能快速定位到对应的代码位置。

Monorepo 结构与包分类

根目录的 package.json 声明了 Yarn v1 workspaces,覆盖 packages/*website 等多个目录。Lerna(lerna.json)负责统一版本管理和发布,当前版本为 3.9.2。

约 40 个包可以归纳为以下几类:

分类 示例 职责
Core docusaurus CLI、服务端流水线、客户端应用、SSG
Bundler docusaurus-bundler Webpack/Rspack 抽象层
内容插件 plugin-content-docsplugin-content-blogplugin-content-pages 读取文件,生成路由
主题 theme-classictheme-common React UI 组件
Preset preset-classic 插件与主题的组合包
MDX mdx-loader 用于 MDX 编译的 Webpack loader
工具库 utilsutils-commonutils-validation 公共辅助函数
类型定义 docusaurus-types TypeScript 类型定义
脚手架 create-docusaurus 项目初始化工具
日志 docusaurus-logger 结构化日志输出
graph TD
    subgraph "Preset Classic"
        PC[preset-classic]
    end
    subgraph "Content Plugins"
        DOCS[plugin-content-docs]
        BLOG[plugin-content-blog]
        PAGES[plugin-content-pages]
    end
    subgraph "Theme Layer"
        TC[theme-classic]
        TCM[theme-common]
    end
    subgraph "Core"
        CORE[docusaurus]
        BUNDLER[docusaurus-bundler]
        MDX[mdx-loader]
    end
    PC --> DOCS
    PC --> BLOG
    PC --> PAGES
    PC --> TC
    TC --> TCM
    DOCS --> MDX
    BLOG --> MDX
    CORE --> BUNDLER

docusaurus 核心包是整个仓库中体量最大的。它包含 CLI、完整的服务端流水线、客户端 React 应用、SSG 引擎以及 webpack 配置层。可以把它理解为内核——其他所有东西都是插入其中的扩展。

提示: 浏览代码库时,从 packages/docusaurus/src/ 入手是最好的起点。服务端代码位于 server/commands/ 目录,客户端代码位于 client/ 目录。这是整个代码库中最重要的目录划分,务必牢记。

两个执行世界:服务端与客户端

在阅读任何代码之前,有一个架构层面的核心概念必须先理解:服务端(Node.js)与客户端(浏览器中的 React)是两套相互独立的代码库,它们通过生成的文件进行通信。

flowchart LR
    subgraph "Server World (Node.js)"
        CONFIG[Config Loading]
        PLUGINS[Plugin Lifecycle]
        CODEGEN[Code Generation]
    end
    subgraph ".docusaurus/"
        GEN[Generated Files]
    end
    subgraph "Client World (React)"
        APP[App Component]
        ROUTES[Routes]
        HYDRATION[Hydration]
    end
    CONFIG --> PLUGINS --> CODEGEN --> GEN
    GEN --> APP
    GEN --> ROUTES
    ROUTES --> HYDRATION

服务端代码在执行 docusaurus builddocusaurus start 时运行。它负责读取配置文件、执行插件生命周期、生成路由清单,并通过 SSG 产出静态 HTML。相关代码位于 packages/docusaurus/src/server/packages/docusaurus/src/commands/

客户端代码是一个在浏览器中完成 hydration 的 React 应用。它使用 React Router 进行导航,按需懒加载路由组件,并管理主题 context。相关代码位于 packages/docusaurus/src/client/

连接两端的桥梁是 .docusaurus/ 目录——这是一个自动生成的文件夹,其中包含 JavaScript 模块、JSON 数据以及路由配置,供客户端 webpack 构建通过 @generated/* 别名引入。服务端负责写入这些文件,客户端负责消费它们。

CLI:全局调度中心

CLI 入口位于 packages/docusaurus/src/commands/cli.ts,使用 Commander.js 定义所有命令。runCLI() 函数负责创建命令程序并解析参数:

flowchart TD
    CLI[runCLI] --> BUILD[build]
    CLI --> START[start]
    CLI --> SWIZZLE[swizzle]
    CLI --> DEPLOY[deploy]
    CLI --> SERVE[serve]
    CLI --> CLEAR[clear]
    CLI --> WT[write-translations]
    CLI --> WHI[write-heading-ids]
    CLI --> EXT{External?}
    EXT -->|Yes| PLUGIN_CMD[Plugin CLI Extensions]

每个命令都对应一个独立模块:build 触发完整的静态构建流水线,start 启动支持热更新的开发服务器,swizzle 处理主题组件的自定义替换。

有一个细节值得关注:CLI 会在 cli.ts#L26-L40 处判断命令是否属于"内部命令"。如果无法识别,会在解析之前调用 externalCommand()——这正是插件注册自定义 CLI 命令的机制。例如,docs 插件就通过这种方式添加了 docs:version 命令,用于创建文档版本快照。

另外值得注意的是第 53-56 行的环境变量配置:DOCUSAURUS_CLI_SITE_DIRDOCUSAURUS_CLI_CONFIG 允许你在不传递 CLI 参数的情况下覆盖站点目录和配置路径。这是因为 Commander.js 在解析完成之前无法确定站点目录,而需要读取配置 context 的插件 CLI 扩展会陷入"先有鸡还是先有蛋"的困境,这两个环境变量正是为此而设的逃生出口。

loadSite() 流水线

packages/docusaurus/src/server/site.ts#L276-L298 中的 loadSite() 函数是整个代码库中最核心的函数。所有需要站点数据的命令——buildstartdeploy——都会调用它。其执行过程如下:

sequenceDiagram
    participant CLI as CLI Command
    participant LS as loadSite()
    participant LC as loadContext()
    participant LP as loadPlugins()
    participant CSP as createSiteProps()
    participant CSF as createSiteFiles()

    CLI->>LS: loadSite(params)
    LS->>LC: Load config, i18n, bundler
    LC-->>LS: LoadContext
    LS->>LP: Run plugin lifecycle (4 phases)
    LP-->>LS: plugins, routes, globalData
    LS->>CSP: Merge routes, metadata, translations
    CSP-->>LS: Props
    LS->>CSF: Generate .docusaurus/ files
    CSF-->>LS: Site ready

整个流水线分为四个阶段:

  1. loadContext()(第 81-173 行):加载站点配置,解析 i18n 语言环境设置,确定输出目录,初始化 bundler(Webpack 或 Rspack),并加载代码翻译文件。

  2. loadPlugins():执行完整的四阶段插件生命周期——初始化、loadContent()contentLoaded()allContentLoaded(),并返回已加载的插件、路由及全局数据。我们将在第二篇文章中详细介绍这一部分。

  3. createSiteProps()(第 175-230 行):将插件的执行结果合并为一个统一的 Props 对象,其中包含路由、元数据、HTML 标签和代码翻译,同时处理重复路由的检测。

  4. createSiteFiles()(第 233-268 行):通过调用 generateSiteFiles() 将文件写入 .docusaurus/ 目录。

提示: Props 类型是服务端流水线与下游所有环节(代码生成、bundler 配置、开发服务器)之间的"契约"。排查构建问题时,首先检查 Props 中的内容往往是最高效的切入点。

.docusaurus/ 桥接层:代码生成

.docusaurus/ 目录是服务端与客户端之间的契约载体。packages/docusaurus/src/server/codegen/codegen.ts#L162-L174 中的 generateSiteFiles() 函数会并行写入所有文件:

生成文件 用途
docusaurus.config.mjs 序列化后的站点配置,供客户端访问
routes.js 使用 ComponentCreator 实现懒加载的 React Router 路由树
registry.js chunk 名称到模块路径的映射,用于代码分割
routesChunkNames.json 路由路径到各路由模块 chunk 名称的映射
client-modules.js 插件的客户端模块(CSS、JS 副作用)
globalData.json 可通过 useGlobalData() 访问的跨插件共享数据
i18n.json 当前语言环境配置
codeTranslations.json UI 字符串翻译
site-metadata.json 插件版本及站点元数据
flowchart TD
    GEN[generateSiteFiles] --> WARN[DONT-EDIT-THIS-FOLDER]
    GEN --> CM[client-modules.js]
    GEN --> SC[docusaurus.config.mjs]
    GEN --> RF[routes.js + registry.js + routesChunkNames.json]
    GEN --> GD[globalData.json]
    GEN --> SM[site-metadata.json]
    GEN --> I18N[i18n.json]
    GEN --> CT[codeTranslations.json]

有一个设计决策值得特别关注:client modules 使用的是 require() 而非 import()。查看 codegen.ts#L68-L77 中的注释可以看到原因——import() 是异步的,但 client modules 可能包含 CSS,而 CSS 的加载顺序直接影响样式优先级。使用同步的 require() 可以确保 CSS 文件按照正确的顺序被打包进 bundle。

路由生成逻辑在 codegenRoutes.ts 中,同样值得深入了解。它会生成三个文件:routes.js 包含使用 ComponentCreator 实现懒加载的精简版 React Router 配置;registry.js 将 chunk 名称映射到带有 webpack magic comments(用于命名 chunk)的动态 import() 调用;routesChunkNames.json 则将路由路径与其对应的 chunk 名称关联起来。这套三文件系统实现了激进的代码分割——每个页面只加载自身所需的 JavaScript。

客户端组件树与路由

客户端 React 应用在 packages/docusaurus/src/client/App.tsx 中完成组装。整个组件树是一个层层嵌套的 provider 结构:

graph TD
    EB[ErrorBoundary] --> DCP[DocusaurusContextProvider]
    DCP --> BCP[BrowserContextProvider]
    BCP --> ROOT["Root (@theme/Root)"]
    ROOT --> TP["ThemeProvider (@theme/ThemeProvider)"]
    TP --> SMD[SiteMetadataDefaults]
    TP --> SM["SiteMetadata (@theme/SiteMetadata)"]
    TP --> BIB[BaseUrlIssueBanner]
    TP --> AN[AppNavigation]
    AN --> PN[PendingNavigation]
    PN --> ROUTES["renderRoutes(@generated/routes)"]

注意,@theme/Root@theme/ThemeProvider 是通过主题别名系统解析的——它们指向 theme-classic 的默认实现,或者用户通过 swizzle 自定义的版本。@generated/routes 导入则直接指向服务端生成的路由文件。

浏览器入口 clientEntry.tsx 同时支持 hydration 和纯客户端渲染两种模式。它会在渲染前预加载当前路径的路由数据,然后根据情况调用 ReactDOM.hydrateRoot()(用于 SSG 页面)或 ReactDOM.createRoot()(用于开发模式)。BrowserRouterHashRouter 之间的选择由 future.experimental_router 配置项控制。

在 SSR/SSG 场景下,serverEntry.tsx 会将同一个 <App /> 包裹在 StaticRouterHelmetProvider 以及 BrokenLinksProvider 中——最后这个 provider 负责收集页面上的所有链接和锚点,用于构建后的链接合法性校验。它将应用渲染为 HTML,并将收集到的元数据连同标记一并返回。

下一步

至此,你已经掌握了整体架构的全局视图:monorepo 的包组织、服务端与客户端的职责划分、CLI 命令分发机制、loadSite() 流水线、.docusaurus/ 桥接层,以及客户端组件树。在下一篇文章中,我们将深入服务端流水线的核心——那个将磁盘上的内容文件转化为浏览器中 React 路由的四阶段插件生命周期。