内容插件与 MDX 管道:从 Markdown 到 React
前置知识
- ›了解插件生命周期与 actions 系统(第 2 篇)
- ›了解构建管道与 webpack loader(第 3 篇)
- ›具备 MDX、remark 和 rehype 生态的基础知识
内容插件与 MDX 管道:从 Markdown 到 React
插件生命周期负责创建路由,构建管道负责编译产物。但磁盘上的一个 .md 文件,究竟是如何变成能在浏览器中渲染的 React 组件的?这正是 MDX 管道的职责所在——一条由 webpack loader 编排、remark 与 rehype 插件共同组成的精密处理链,以及三个负责读取文件并将其映射到路由的内容插件。
本文将完整追踪内容处理管道的全过程:从原始 Markdown 文件到最终渲染的页面。我们先从 MDX loader 入手,逐一梳理每个 remark 插件的执行顺序,再以 docs 插件为案例,深入分析内容插件如何协调文件读取、版本管理、侧边栏生成和路由创建。
MDX Webpack Loader
Markdown 处理的入口是位于 packages/docusaurus-mdx-loader/src/loader.ts 的 webpack loader。当 webpack 遇到 .md 或 .mdx 文件时,该 loader 会依次执行以下步骤:
- 解析 front matter — 使用可配置的
parseFrontMatter函数(第 44-48 行) - 将 MDX 编译为 JSX — 通过
compileToJSX()运行完整的 remark/rehype 处理链 - 提取内容标题 — 从第一个标题节点中获取
- 检测 MDX 局部文件 — 以
_为前缀的文件被视为局部文件(partials),若其中包含 front matter 则会触发警告 - 生成资源引用 — Markdown 中引用的图片和链接会被转换为 webpack 的
require()调用
flowchart TD
MD["document.md"] --> LOADER["MDX Loader"]
LOADER --> FM["Parse front matter"]
FM --> COMPILE["compileToJSX()"]
COMPILE --> REMARK["Remark plugins"]
REMARK --> REHYPE["Rehype plugins"]
REHYPE --> JSX["JSX output"]
JSX --> ASSETS["Asset require() emission"]
ASSETS --> MODULE["Webpack module"]
loader 会区分两种 webpack compiler 名称(client 和 server)。当启用 mdxCrossCompilerCache 时,同一个 MDX 文件的编译结果可以在两者之间复用——这是一项重要的性能优化,因为构建过程中每个 MDX 文件会被编译两次(一次用于客户端 bundle,一次用于 SSG)。
Remark/Rehype 插件链
位于 processor.ts 的处理器工厂函数负责组装完整的转换链。插件的执行顺序至关重要——每个插件对 AST 的处理结果都会传递给下一个插件。
remark 插件的执行顺序如下:
flowchart TD
A["beforeDefaultRemarkPlugins (user)"] --> B["remark-frontmatter"]
B --> C["remark-directive"]
C --> D["contentTitle (extract h1)"]
D --> E["admonitions (:::note, :::tip)"]
E --> F["headings (extract + generate IDs)"]
F --> G["remark-emoji"]
G --> H["toc (table of contents)"]
H --> I["details (HTML details/summary)"]
I --> J["head (HTML head injection)"]
J --> K["mermaid (if enabled)"]
K --> L["transformImage (require assets)"]
L --> M["resolveMarkdownLinks"]
M --> N["transformLinks (require assets)"]
N --> O["remark-gfm"]
O --> P["remark-comment (if mdx1Compat)"]
P --> Q["remarkPlugins (user)"]
Q --> R["unusedDirectivesWarning"]
R --> S["codeCompatPlugin (MDX1 code blocks)"]
每个插件都有其特定用途,以下几个值得重点关注:
headings(remark/headings)负责提取所有标题并生成稳定的锚点 ID。anchorsMaintainCase 选项控制 ID 是否转为小写。
toc(remark/toc)根据标题构建目录数据结构,并将其作为 toc 元数据注入,供主题组件使用。
transformImage 和 transformLinks 将  和 [link](./doc.md) 中的相对路径转换为 webpack 的 require() 调用,使 bundler 能够处理并为这些资源生成哈希指纹。
admonitions(remark/admonitions)将 :::note / :::tip / :::warning 指令块转换为 <Admonition> JSX 组件。
unusedDirectivesWarning 负责捕获未被任何插件处理的类指令语法(如 :::something),并向用户发出警告,提示可能存在拼写错误。
codeCompatPlugin 处理 MDX v1 代码块的兼容性问题——它始终在最后执行,确保在用户插件(如 npm2yarn)之后运行。
remark 插件执行完毕后,rehype 插件开始运行。对于 md 格式(区别于 mdx),会在开头插入 rehype-raw 以处理原始 HTML 块,同时对 MDX 表达式节点进行透传。
提示: 你可以在
docusaurus.config.js中通过内容插件选项(如docs: {remarkPlugins: [...]})添加自定义 remark/rehype 插件。自定义插件会被插入到默认 remark 插件与unusedDirectivesWarning插件之间,此时 AST 已经过充分处理,结构清晰。
Docs 插件:内容加载与版本管理
位于 plugin-content-docs/src/index.ts#L82-L108 的 docs 插件是最复杂的内容插件。其构造函数会立即解析侧边栏路径并读取版本元数据:
const versionsMetadata = await readVersionsMetadata({context, options});
版本管理系统支持同时维护多个文档版本。版本元数据包含版本名称、内容路径、侧边栏路径和 URL 前缀。"当前"版本默认指向 docs/ 目录,而已发布的历史版本则存储在 versioned_docs/v<X>/ 中,并通过 versions.json 进行追踪。
flowchart TD
VM["readVersionsMetadata()"] --> CURRENT["Current version<br/>docs/"]
VM --> V2["Version 2.0<br/>versioned_docs/version-2.0/"]
VM --> V1["Version 1.0<br/>versioned_docs/version-1.0/"]
CURRENT --> LOAD["loadVersion()"]
V2 --> LOAD
V1 --> LOAD
LOAD --> DOCS["Per-version docs array"]
LOAD --> SIDEBARS["Per-version sidebars"]
在 loadContent() 阶段,插件会对每个版本调用 loadVersion(),扫描内容目录中的 Markdown 文件,解析其 front matter,提取元数据(标题、描述、标签、侧边栏位置),并构建完整的文档数据结构。每个版本都有独立的侧边栏配置和内容路径。
通过 id 选项,插件还支持多实例运行——你可以使用 docs 作为主文档实例,同时创建一个名为 community 的第二实例,指向不同的内容路径。每个实例都拥有独立的版本管理、侧边栏和路由。
侧边栏生成
sidebars/index.ts 中的侧边栏系统支持两种模式:
自动生成侧边栏 — 默认模式。当 sidebarPath 为 undefined 时,Docusaurus 根据文件系统结构自动生成侧边栏。第 22-29 行的 DefaultSidebars 配置会创建一个 type: 'autogenerated' 类型的侧边栏,指向根目录。
自动生成功能支持通过 _category_.json 或 _category_.yml 文件自定义分类的标签、排序和描述。readCategoriesMetadata() 函数(第 43-68 行)使用 glob 模式扫描这些文件。
手动侧边栏 — 通过导出侧边栏配置的 sidebars.js 文件定义,使用 loadFreshModule() 加载,支持 TypeScript 和 ESM 格式。
两种模式都经过相同的处理管道:规范化 → 处理 → 后处理。处理阶段会通过扫描文件系统将 autogenerated 条目解析为实际的文档引用,后处理阶段则负责验证链接目标并应用排序规则。
graph TD
INPUT["sidebarPath option"] -->|undefined| AUTO["DefaultSidebars<br/>autogenerated from filesystem"]
INPUT -->|false| DISABLED["DisabledSidebars<br/>no sidebar"]
INPUT -->|path| MANUAL["Load sidebars.js"]
AUTO --> NORM["normalizeSidebars()"]
MANUAL --> NORM
NORM --> PROC["processSidebars()"]
PROC --> POST["postProcessSidebars()"]
POST --> FINAL["Final sidebar data"]
路由创建:将内容与主题组件连接
内容插件中最直观的部分莫过于 contentLoaded() 的实现。docs 插件在 routes.ts 中的路由创建逻辑,清晰展示了内容是如何转化为路由的。
对于每篇文档,插件会:
- 调用
actions.createData()将文档元数据写入一个 JSON 模块 - 创建路由,
component设为options.docItemComponent(解析为@theme/DocItem) - 设置
modules: {content: doc.source}— 指示路由将 MDX 文件作为模块加载 - 将
sidebar作为路由属性附加,供父组件渲染对应的侧边栏
sequenceDiagram
participant DOC as docs plugin
participant ACT as actions
participant GEN as .docusaurus/
participant THEME as @theme/DocItem
DOC->>ACT: createData('abc123.json', docMetadata)
ACT->>GEN: Write JSON to .docusaurus/docs/default/abc123.json
ACT-->>DOC: modulePath
DOC->>ACT: addRoute({path, component: '@theme/DocItem', modules: {content: doc.source}})
Note over DOC,THEME: At runtime, DocItem receives doc content and metadata as props
路由结构是层级化的:每个版本创建一个以 docRootComponent(解析为 @theme/DocRoot)为组件的父路由,各个文档页面作为嵌套子路由挂载其下。这种设计使侧边栏组件在 DocRoot 层级渲染,在文档间导航时无需卸载重载,从而保持持久化状态。
提示:
addRoute()中的component字段始终是字符串,如'@theme/DocItem',而非实际的 import 语句。主题别名系统(将在第 5 篇中介绍)会在 webpack 编译阶段将该字符串解析为真正的 React 组件。
Blog 插件与 Pages 插件
docs 插件虽然最为复杂,但 blog 和 pages 插件遵循相同的设计模式:
plugin-content-blog 扫描博客文章(遵循日期命名约定的 Markdown 文件),为单篇文章、博客列表、标签页和归档页分别生成路由,使用 @theme/BlogPostPage、@theme/BlogListPage 等主题组件。
plugin-content-pages 是最简单的一个——它扫描 src/pages/ 目录下的 React 组件和 MDX 文件,为 MDX 内容创建使用 @theme/MDXPage 的路由。没有版本管理,没有侧边栏,只是直接的文件到路由映射。
三个插件共享一些通用模式:通过 getPathsToWatch() 告知开发服务器哪些文件变更需要触发重新构建;通过 configureWebpack() 为各自的内容目录注册 MDX loader 规则;通过 getTranslationFiles() 支持 i18n。
MDX loader 规则是内容插件与 MDX 管道之间的关键纽带。每个插件都会注册一条 webpack 规则,匹配其内容目录下的 .md/.mdx 文件,并使用 MDX loader 及其特定配置(remark 插件、admonition 设置等)进行处理。这也正是我们在第 2 篇中看到的 MDX fallback 插件需要排除这些路径的原因——它只处理其余的文件。
下一步
我们已经完整追踪了内容从 Markdown 文件经过 MDX 管道到路由创建的全过程。但路由中引用的是 @theme/DocItem 这样的主题组件字符串——它是如何变成真正的 React 组件的?下一篇文章将深入探讨主题系统的别名解析机制、theme-classic 中 100 多个组件的组织方式,以及让 Docusaurus 独具特色的 swizzle 机制。