Storybook Docs MDX 深入解析:用 Doc Blocks 在同一文件中编写文档与 Story
本篇指南以 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 画布与代码片段共处一页:
这个示例中“同时发生”了好几件事:你在写 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 以分组形式展示:
这里有两个值得注意的行为:
<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 里,有两种模式:
- 不定义
<Meta>:可以只写 Markdown,并将其与某个已存在的 story 关联(参考 recipes.md 中的 "CSF Stories with MDX Docs" 配方)。 - 定义
<Meta>但不定义任何 story:得到所谓的 "documentation-only story"——该文件在 UI 中会作为一个独立的文档节点出现:
MDX 文件命名约定
除非使用自定义 webpack 配置,所有 MDX 文件都应当以 *.stories.mdx 作为后缀。这个后缀会告诉 Storybook 对该文件中的 <Meta> 与 <Story> 元素应用特殊处理。
同时记得更新 Storybook 配置,使其加载 .stories.mdx 文件(按 addon-docs 安装说明 操作),典型做法是在 main.js 的 stories 字段中加入 glob,例如 ./stories/**/*.mdx。
源码级解析:MDX 是如何被编译和渲染的
上面是文档层面的用法;结合仓库源码,可以看到这些行为背后的实现。
编译管线:webpack loader 与 Vite 插件
addon-docs 对 MDX 的处理入口是统一的编译函数 compile(),位于 compiler/index.ts。它对不同构建系统暴露两个适配器:
- Webpack:mdx-loader.ts 中的 loader 从 loader 上下文取出
filepath(即被编译文件路径)与编译选项,调用compile(content, options),并在产物前注入import React from 'react';(DEFAULT_RENDERER)再回调给 webpack。 - Vite:mdx-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 默认导出的全部字段,如 title、component、decorators、parameters)外加一个 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 高度默认100px;inline={true}时 story 直接内联渲染,可配合height、autoplay控制高度与 play 函数执行。- 加载中的骨架屏:story 数据尚未就绪时渲染
<StorySkeleton />;若 story 的parameters.docs.disable为真,则整个块被跳过(返回null)。
参考资料
- addon-docs 总览:README
- 相关文档:DocsPage / FAQ / Recipes / Theming / Props Tables
- 各框架支持文档:React / Vue 3 / Angular / Web Components / Ember
- 核心源码:mdx-loader / mdx-plugin / Meta 块 / Story 块 / Canvas 块
掌握本文内容后,你可以按以下路径继续深入:先用基本示例跑通一个 *.stories.mdx 文件,再尝试为现有 CSF 组件补一个 documentation-only 页面,最后阅读 blocks/blocks/ 目录下的其余 Doc Blocks(Canvas、Source、Primary、ArgTypes、Controls),它们与 <Meta>/<Story> 一起构成了 Storybook 文档页的完整组件集。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


