首页
/ Storybook Docs MDX 深入解析:用 Doc Blocks 在同一文件中编写文档与 Story

Storybook Docs MDX 深入解析:用 Doc Blocks 在同一文件中编写文档与 Story

2026-09-06 23:40:12作者:乔或婵

本篇指南以 Storybook 仓库中的 addons/docs 官方文档 mdx.md 为核心,完整讲解 MDX(Markdown + JSX)在 Storybook Docs 中的工作机制:如何用 <Meta><Story><Canvas> 这三个 Doc Blocks 在文档页中定义 story、嵌入已有 story、配置 decorators/parameters,以及纯文档型 MDX 页的写法。读完本文,你不仅能直接复制文中的示例搭建自己的 MDX 文档页,还能结合 addon-docs 源码 理解 MDX 文件从编译到渲染的完整链路。

MDX 是什么:MDX-Flavored CSF

MDX 是一种将 Markdown 与 JSX 结合的标准文件格式:你可以用 Markdown 的简洁语法(如 # heading)书写长文档,并在文件任意位置自由嵌入 JSX 组件块。

Storybook 在其之上定义了 MDX-Flavored CSF(MDX 风格的 Component Story Format),核心是一组被称为 "Doc Blocks" 的组件。它们允许 Storybook 将 MDX 文件翻译成标准的 storybook stories。由于 MDX 中定义的 story 与普通 Storybook story 完全一致,因此可以无缝使用 Storybook 生态中的所有 addons 和视图层。

这种一一对应关系是关键设计:MDX 代码到 CSF 是一一映射的,而 CSF 又直接对应 Storybook 内部的 storiesOf API。对使用者而言,已有的 Storybook 知识可以在三者之间自由迁移;对实现而言,底层转换因此简单且可预测。

例如,下文基本示例中的 story,用 CSF 重写后就是:

import React from 'react';
import { Checkbox } from './Checkbox';
export default { title: "MDX/Checkbox" component: Checkbox };
export const allCheckboxes = () => (
  <form>
    <Checkbox id="Unchecked" label="Unchecked" />
    <Checkbox id="Checked" label="Checked" checked />
    <Checkbox appearance="secondary" id="second" label="Secondary" checked />
  </form>
);

基本示例:Markdown 与 Story 的混合文档

下面是一个结合 Markdown 与单个 story 的最小示例:

import { Meta, Story, Canvas } from '@storybook/addon-docs';
import { Checkbox } from './Checkbox';

<Meta title="MDX/Checkbox" component={Checkbox} />

# Checkbox

With `MDX` we can define a story for `Checkbox` right in the middle of our
markdown documentation.

<Canvas>
  <Story name="all checkboxes">
    <form>
      <Checkbox id="Unchecked" label="Unchecked" />
      <Checkbox id="Checked" label="Checked" checked />
      <Checkbox appearance="secondary" id="second" label="Secondary" checked />
    </form>
  </Story>
</Canvas>

在 Storybook 中的渲染效果如下——可以看到 Markdown 正文、可交互的 story 画布与代码片段共处一页:

MDX 基本示例渲染效果:Checkbox 组件文档页与内嵌 story 画布

这个示例中“同时发生”了好几件事:你在写 Markdown,在写 JSX,同时还定义了一个与 Storybook 整个生态完全兼容的 story。

编写 Stories:从单个定义到分组预览

下面看一个更贴近实际的例子,展示 Doc Blocks 的更多能力:

import { Meta, Story, Canvas } from '@storybook/addon-docs';

import { Badge } from './Badge';
import { Icon } from './Icon';

<Meta title="MDX/Badge" component={Badge} />

# Badge

Let's define a story for our `Badge` component:

<Story name="positive">
  <Badge status="positive">Positive</Badge>
</Story>

We can drop it in a `Canvas` to get a code snippet:

<Canvas>
  <Story name="negative">
    <Badge status="negative">Negative</Badge>
  </Story>
</Canvas>

We can even preview multiple stories in a block. This
gets rendered as a group, but defines individual stories
with unique URLs and isolated snapshot tests.

<Canvas>
  <Story name="warning">
    <Badge status="warning">Warning</Badge>
  </Story>
  <Story name="neutral">
    <Badge status="neutral">Neutral</Badge>
  </Story>
  <Story name="error">
    <Badge status="error">Error</Badge>
  </Story>
  <Story name="with icon">
    <Badge status="warning">
      <Icon icon="check" inline />
      with icon
    </Badge>
  </Story>
</Canvas>

渲染后的页面如下,多个 story 以分组形式展示:

Badge 组件 MDX 文档页:多个 story 以分组画布形式渲染

这里有两个值得注意的行为:

  • <Story> 裸写 vs. 包在 <Canvas>:裸写的 <Story> 直接渲染 story;包进 <Canvas> 后额外获得一个可复制的代码片段(code snippet)区域。
  • 一个 <Canvas> 内可以放多个 <Story>:这在视觉上是一组,但在 Storybook 内部却是各自独立、拥有唯一 URL 的 story,并可以分别执行隔离的快照测试。

嵌入已有 Stories

假设你已经有一个现成的 story,想把它嵌入到 MDX 文档中,可以用 id 属性按 story ID 引用(在 Storybook v5+ 中,查看浏览器 URL 即可得到 story 的 ID):

import { Story } from "@storybook/addon-docs";

# Some header

And markdown here

<Story id="some--id" />

嵌入方式可以和 MDX 的其他特性组合使用,包括 source 代码展示、preview 预览和 props 表格。

Decorators 与 Parameters

在 MDX 中为 story 添加 decorators 和 parameters 的方式,与普通 CSF 文件一致——把它们作为 props 传给 <Meta><Story>

<Meta
  title='MyComponent'
  decorators={[ ... ]}
  parameters={{ ... }}
/>

<Story name="story" decorators={[ ... ]} parameters={{ ... }} >
...
</Story>

此外,全局 decorators 的用法也不变,例如在 .storybook/preview.js 中添加:

import { addDecorator, addParameters } from '@storybook/react';

addDecorator(...);
addParameters({ ... });

Documentation-only MDX:纯文档页

通常使用 Storybook MDX 时,你在 MDX 中定义的 stories 会自动与文档关联。但如果你只想写 Markdown 风格的文档并让它出现在 Storybook 里,有两种模式:

  1. 不定义 <Meta>:可以只写 Markdown,并将其与某个已存在的 story 关联(参考 recipes.md 中的 "CSF Stories with MDX Docs" 配方)。
  2. 定义 <Meta> 但不定义任何 story:得到所谓的 "documentation-only story"——该文件在 UI 中会作为一个独立的文档节点出现:

Documentation-only MDX 页面在 Storybook 导航中作为独立文档节点展示

MDX 文件命名约定

除非使用自定义 webpack 配置,所有 MDX 文件都应当以 *.stories.mdx 作为后缀。这个后缀会告诉 Storybook 对该文件中的 <Meta><Story> 元素应用特殊处理。

同时记得更新 Storybook 配置,使其加载 .stories.mdx 文件(按 addon-docs 安装说明 操作),典型做法是在 main.jsstories 字段中加入 glob,例如 ./stories/**/*.mdx

源码级解析:MDX 是如何被编译和渲染的

上面是文档层面的用法;结合仓库源码,可以看到这些行为背后的实现。

编译管线:webpack loader 与 Vite 插件

addon-docs 对 MDX 的处理入口是统一的编译函数 compile(),位于 compiler/index.ts。它对不同构建系统暴露两个适配器:

  • Webpackmdx-loader.ts 中的 loader 从 loader 上下文取出 filepath(即被编译文件路径)与编译选项,调用 compile(content, options),并在产物前注入 import React from 'react';DEFAULT_RENDERER)再回调给 webpack。
  • Vitemdx-plugin.ts 中的 mdxPlugin 使用 createFilter(/\.mdx$/) 只拦截 .mdx 文件。它通过 presets.apply('mdxLoaderOptions', ...) 允许用户经 preset 定制编译参数,并默认设置:
    • providerImportSource 指向 mdx-react-shim——MDX 的组件解析不直接依赖宿主项目的 React 包,而是走 shim,从而与 Storybook 预览环境的 React 实例保持一致;
    • rehypePlugins 中追加 rehypeSlug(为标题生成锚点,支撑文档页内的目录导航)与 rehypeExternalLinks(为外链添加标记)。

这也解释了“文件必须以 *.stories.mdx 结尾”这条约定:Storybook 的 stories glob 匹配与这套 loader/plugin 过滤共同决定了哪些文件会走 Doc Blocks 转换。

<Meta>:底层就是默认导出

blocks/blocks/Meta.tsx 中的注释直接点明了它的本质:该组件“在底层被转换为一个 default export”。<Meta> 接收 BaseAnnotations(即 CSF 默认导出的全部字段,如 titlecomponentdecoratorsparameters)外加一个 of 属性——当你想引用另一份 CSF 文件的导出作为元数据时,Meta 会调用 context.referenceMeta(of, true) 完成关联。渲染时它通过 context.storyById() 找到主 story 并返回一个指向该 story 的 <Anchor>;在“未绑定”的 MDX 文件里使用 <Meta> 时则直接返回 null,这正是 documentation-only 模式能成立的原因。

<Story>:从 props 到 storyId 的解析

blocks/blocks/Story.tsx 揭示了嵌入 story 的完整决策逻辑:

  • of / meta 属性<Story of={ButtonStories.Primary} /> 可直接引用导出的 story;若 MDX 文件是“未绑定”的,还需同时传 meta={ButtonStories} 提供元数据上下文。getStoryId()L63-L74)通过 context.resolveOf(of || 'story', ['story']) 解析出目标,id 属性则等价于按 ID 查找。
  • inline / height / autoplay 参数getStoryProps()L76-L116)实现了明确的优先级——先取块级 props,再取 story 的 parameters.docs.story 配置,最后才是默认值。默认是 iframe 模式(inline 缺省为 false),iframe 高度默认 100pxinline={true} 时 story 直接内联渲染,可配合 heightautoplay 控制高度与 play 函数执行。
  • 加载中的骨架屏:story 数据尚未就绪时渲染 <StorySkeleton />;若 story 的 parameters.docs.disable 为真,则整个块被跳过(返回 null)。

参考资料

掌握本文内容后,你可以按以下路径继续深入:先用基本示例跑通一个 *.stories.mdx 文件,再尝试为现有 CSF 组件补一个 documentation-only 页面,最后阅读 blocks/blocks/ 目录下的其余 Doc Blocks(CanvasSourcePrimaryArgTypesControls),它们与 <Meta>/<Story> 一起构成了 Storybook 文档页的完整组件集。

登录后查看全文
热门项目推荐
相关项目推荐