~/ ?.log $
返回文章列表
11 min read
更新于 2026年3月6日

我不知道的 dumi(01)— 从 Markdown 到文档站点:SSG 流程与约定式路由

用 dumi 写组件文档,最让人觉得"神奇"的体验之一是:在 docs/ 目录下放一个 guide.md,启动 dumi dev,浏览器里就出现了一个 /guide 页面,还带侧边栏导航。不需要配路由,不需要写页面组件,甚至不需要改任何配置。

一、放一个 .md 文件,路由就有了?

用 dumi 写组件文档,最让人觉得”神奇”的体验之一是:在 docs/ 目录下放一个 guide.md,启动 dumi dev,浏览器里就出现了一个 /guide 页面,还带侧边栏导航。不需要配路由,不需要写页面组件,甚至不需要改任何配置。

但这背后并不是”魔法”——从 Markdown 文件到可交互的文档页面,dumi 经历了一条完整的构建链路:文件扫描、路由生成、Markdown 解析(AST 转换)、代码块编译、静态 HTML 输出。这条链路的每一环,都建立在 Umi 框架的基础能力之上。


二、dumi 和 Umi 的关系:不是”基于”,而是”寄生”

很多人知道 dumi 基于 Umi,但”基于”这个词太笼统了。更准确的说法是:dumi 是 Umi 的一个 preset(预设插件集),它没有自己的构建引擎、路由系统、插件机制——这些全部复用 Umi 的。

dumi 做的事情是在 Umi 的框架上注入一组专门为”组件文档”场景设计的插件:

// .umirc.ts — dumi 的配置文件,本质就是 Umi 配置
import { defineConfig } from 'dumi';
export default defineConfig({
  resolve: { includes: ['docs', 'src'] },
  themeConfig: { name: 'My Lib' },
});

resolve.includes 告诉 dumi 从哪些目录扫描文档文件。除此之外,Umi 的所有配置(路由、构建、代理、环境变量)在 dumi 中都可以直接使用。

说白了,dumi 就是一个”文档专用的 Umi 配置包”。理解了这一点,很多 dumi 的行为就不难解释了——因为底层逻辑全是 Umi 的。


三、文件到路由:约定式路由的完整流程

dumi 的路由生成遵循”约定大于配置”的原则。整个流程可以拆分为四步:

第一步:文件扫描

启动时,dumi 通过 Umi 内置的 glob 模块扫描 resolve.includes 指定目录下的所有 .md/.mdx 文件,忽略 node_modules、以 _ 开头的文件等。

扫描结果是一个文件路径列表:

docs/
├── index.md         → /
├── guide.md         → /guide
├── guide/
│   ├── index.md     → /guide
│   └── advanced.md  → /guide/advanced
└── components/
    └── button.md    → /components/button

第二步:元数据解析

对每个 Markdown 文件,dumi 用 gray-matter 库解析文件头部的 YAML frontmatter:

---
title: 快速上手
order: 1
nav: 指南
---

# 内容...

这里的 titleordernav 就是路由的元数据。order 决定侧边栏排序,nav 决定文件归属到哪个顶部导航分组。

这里有一个很多人会忽略的细节——如果不写 frontmatter,dumi 会从文件的第一个 # 标题中提取 title。也就是说 frontmatter 不是必须的,但要精确控制路由行为(排序、分组、路径别名),就需要显式声明。

第三步:路由对象生成

解析完文件路径和元数据后,dumi 通过 Umi 的 api.modifyRoutes 钩子将这些信息注入到路由系统中。每个 Markdown 文件对应一个路由对象:

{
  path: '/guide',
  component: 'docs/guide.md',
  exact: true,
  meta: {
    title: '快速上手',
    order: 1,
    nav: '指南',
    filePath: 'docs/guide.md'
  }
}

对多级嵌套目录,dumi 会自动生成嵌套路由结构。子目录下的 index.md 作为该层级的默认页面。

第四步:路由优先级

当路径存在冲突时(比如 docs/guide.mddocs/guide/index.md 都映射到 /guide),dumi 的优先级规则是:

(1) index.md 作为目录入口,优先级高于同名文件

(2) order 值小的路由在侧边栏中排在前面

(3) 精确路径匹配优先于模糊匹配


四、Markdown 解析:unified 生态的组装

路由确定了”哪些页面存在”,接下来的问题是”每个页面长什么样”。dumi 的 Markdown 解析基于 unified 生态——一条从 Markdown 文本到 React 组件的转换管线。

核心流程:

(1) remark 将 Markdown 文本解析为 MDAST(Markdown 抽象语法树)

(2) dumi 的自定义 remark 插件识别代码块中的 JSX/TSX,标记为”需要动态渲染”

(3) rehype 将 MDAST 转换为 HAST(HTML 抽象语法树)

(4) rehype-react 将 HAST 转换为 React 元素

(5) 最终输出的 React 组件就是这个页面的内容

这里最关键的一步是代码块处理。普通的 Markdown 代码块会被渲染为静态高亮代码,但在 dumi 中,带有特定标记(如 ```jsx```tsx)的代码块会被编译为可交互的组件预览:

function processCodeBlock(content, meta) {
  if (meta.lang === 'jsx' || meta.lang === 'tsx') {
    const compiled = sucrase.transform(content, {
      transforms: ['jsx', 'typescript'],
    });
    return createLivePreview(compiled.code);
  }
  return createStaticHighlight(content, meta.lang);
}

sucrase 是一个轻量级的 JavaScript/TypeScript 编译器,比 Babel 更快但功能更少——对于文档中的组件预览场景,这个取舍是合理的。

换句话说,dumi 的 Markdown 不是普通的 Markdown——代码块既是文档,也是可运行的组件实例。这是 dumi 区别于 VitePress、Docusaurus 等通用文档工具的核心差异。


五、构建输出:esbuild 加速与静态 HTML

解析完成后,dumi 的构建流程与 Umi 一致:

开发模式dumi dev):使用 Webpack/Mako 进行增量编译,代码修改后热更新。Markdown 文件变动时,只重新解析变动的文件,不需要全量重建。

生产模式dumi build):输出静态 HTML + CSS + JS 文件。每个路由对应一个 HTML 文件,可以直接部署到 CDN 或 GitHub Pages。

Umi 4 引入了对 esbuild 和 Mako(字节跳动开源的 Rust 构建工具)的支持,显著提升了构建速度。dumi 作为 Umi 的 preset,自动继承了这些优化:

// .umirc.ts
export default defineConfig({
  mako: {}, // 启用 Mako 构建
});

六、插件系统:在构建流程中”插针”

dumi 的可扩展性来自 Umi 的插件系统。几个常用的扩展点:

api.modifyRoutes:修改路由表。比如为所有路由添加多语言前缀:

export default (api: IApi) => {
  api.modifyRoutes((routes) => {
    const enRoutes = routes.map((route) => ({
      ...route,
      path: `/en${route.path}`,
      meta: { ...route.meta, lang: 'en' },
    }));
    return [...routes, ...enRoutes];
  });
};

api.onGenerateFiles:在构建阶段生成临时文件。比如自动从 TypeScript 类型定义中提取组件 API 文档。

api.modifyTsConfig:修改 TypeScript 配置。

这些钩子的执行时机都在 Umi 的构建流水线中有明确的位置。说白了,dumi 的插件就是 Umi 的插件——任何 Umi 插件教程中学到的技巧,在 dumi 中都适用。


七、与 Storybook 的路线差异

dumi 和 Storybook 都是组件文档工具,但技术路线截然不同:

dumi 是 Markdown 驱动:文档和组件预览写在 .md 文件中,构建时通过 SSG 输出静态站点。

Storybook 是 Story 驱动:每个组件的展示状态写在 .stories.js 中,运行时动态渲染。

维度dumiStorybook
文档格式Markdown/MDX.stories.js/ts
构建方式SSG(静态输出)动态运行时
组件预览代码块即预览Story 配置
启动速度快(esbuild/Mako)较慢(Webpack)
中文生态好(蚂蚁出品)一般
交互测试强(addon 丰富)

如果项目是国内的组件库,文档以中文为主,且需要”写完 Markdown 就有文档站”的体验——dumi 是更自然的选择。如果项目需要丰富的交互测试(视觉回归、无障碍测试)和国际化生态——Storybook 更合适。

问题的关键在于——它们解决的不完全是同一个问题。dumi 偏向”文档”,Storybook 偏向”组件开发工作台”。在大型项目中,两者共存也不少见。


八、总结

dumi 的”自动路由 + Markdown 即组件预览”的能力,底层是一条 Umi 框架上的完整构建链路:

文件扫描 → frontmatter 元数据解析 → 路由对象生成 → unified 管线解析 Markdown → sucrase 编译代码块 → 静态 HTML 输出

如果你只记住一句话:dumi 不是一个独立的文档工具,而是 Umi 的一个 preset——它的路由、构建、插件机制全部来自 Umi。 搞清楚这一点,dumi 中遇到的大部分问题,都可以在 Umi 的文档和社区中找到答案。


延伸阅读:


本系列其他文章:

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;