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-docs、plugin-content-blog、plugin-content-pages |
读取文件,生成路由 |
| 主题 | theme-classic、theme-common |
React UI 组件 |
| Preset | preset-classic |
插件与主题的组合包 |
| MDX | mdx-loader |
用于 MDX 编译的 Webpack loader |
| 工具库 | utils、utils-common、utils-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 build 和 docusaurus 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_DIR 和 DOCUSAURUS_CLI_CONFIG 允许你在不传递 CLI 参数的情况下覆盖站点目录和配置路径。这是因为 Commander.js 在解析完成之前无法确定站点目录,而需要读取配置 context 的插件 CLI 扩展会陷入"先有鸡还是先有蛋"的困境,这两个环境变量正是为此而设的逃生出口。
loadSite() 流水线
packages/docusaurus/src/server/site.ts#L276-L298 中的 loadSite() 函数是整个代码库中最核心的函数。所有需要站点数据的命令——build、start、deploy——都会调用它。其执行过程如下:
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
整个流水线分为四个阶段:
-
loadContext()(第 81-173 行):加载站点配置,解析 i18n 语言环境设置,确定输出目录,初始化 bundler(Webpack 或 Rspack),并加载代码翻译文件。 -
loadPlugins():执行完整的四阶段插件生命周期——初始化、loadContent()、contentLoaded()和allContentLoaded(),并返回已加载的插件、路由及全局数据。我们将在第二篇文章中详细介绍这一部分。 -
createSiteProps()(第 175-230 行):将插件的执行结果合并为一个统一的Props对象,其中包含路由、元数据、HTML 标签和代码翻译,同时处理重复路由的检测。 -
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()(用于开发模式)。BrowserRouter 与 HashRouter 之间的选择由 future.experimental_router 配置项控制。
在 SSR/SSG 场景下,serverEntry.tsx 会将同一个 <App /> 包裹在 StaticRouter、HelmetProvider 以及 BrokenLinksProvider 中——最后这个 provider 负责收集页面上的所有链接和锚点,用于构建后的链接合法性校验。它将应用渲染为 HTML,并将收集到的元数据连同标记一并返回。
下一步
至此,你已经掌握了整体架构的全局视图:monorepo 的包组织、服务端与客户端的职责划分、CLI 命令分发机制、loadSite() 流水线、.docusaurus/ 桥接层,以及客户端组件树。在下一篇文章中,我们将深入服务端流水线的核心——那个将磁盘上的内容文件转化为浏览器中 React 路由的四阶段插件生命周期。