Storybook React 文档实战:用 @storybook/addon-docs 自动生成 DocsPage、Props 表与 MDX 长文档
本篇技术指南基于 Storybook 仓库中的 React 框架文档 code/addons/docs/docs/frameworks/REACT.md,系统讲解如何为 React 项目接入 @storybook/addon-docs:安装与主配置、DocsPage 自动生成机制、基于 component 元数据的 Props 表、MDX 富文档写法、内联/iframe 故事渲染策略,以及 react-docgen 与 react-docgen-typescript 两种 docgen 方案的取舍。读完后你可以直接在 React 项目中复制配置得到"开箱即用"的组件文档,并能从源码层面理解每个参数背后的实现。
一、@storybook/addon-docs 在 React 项目中的工作方式
Storybook Docs 将你的 Storybook 故事(stories)转换为结构化的组件文档。对 React 而言,它同时支持两种形态:
- DocsPage:零配置、自动生成的文档页,出现在 Storybook UI 的
Docs标签中; - MDX:以 Markdown 书写、可内嵌故事与 Props 表等文档组件的长文档。
从源码结构看,两者由 @storybook/react 渲染器在预览层"接线"。React 渲染器的预设会根据 docs 配置是否启用,动态追加预览注解(annotations):
// code/renderers/react/src/preset.ts (L49-L57)
return result
.concat(input)
.concat([
fileURLToPath(import.meta.resolve('@storybook/react/entry-preview')),
fileURLToPath(import.meta.resolve('@storybook/react/entry-preview-argtypes')),
])
.concat(
docsEnabled ? [fileURLToPath(import.meta.resolve('@storybook/react/entry-preview-docs'))] : []
)
也就是说,只有当 docs 预设实际返回配置时,entry-preview-docs 才会被注入预览运行时。而 @storybook/addon-docs 自身的 docs 预设属性则负责声明默认的文档页名称 Docs:
// code/addons/docs/src/preset.ts (L139-L154)
const docs: PresetProperty<'docs'> = (input = {}, options) => {
if (options?.build?.test?.disableAutoDocs) {
return undefined;
}
const result: StorybookConfigRaw['docs'] = {
...input,
defaultName: 'Docs',
};
// ...
};
这解释了两个事实:文档页默认叫 "Docs"(可通过 docs.defaultName 覆盖),以及构建测试时可以通过 disableAutoDocs 特性彻底关闭自动文档生成。
二、安装与主配置
首先添加包,并确保你的 @storybook/* 各包版本一致:
yarn add -D @storybook/addon-docs
然后在 .storybook/main.js 的 addons 列表中注册:
export default {
// other settings
addons: ['@storybook/addon-docs'];
};
安装完成后,你应该立即为所有故事获得基础的 DocsPage 文档,入口就是 Storybook UI 中的 Docs 标签。
底层细节:React 单例与包去重
Docs 的文档页与故事块(blocks)都是 React 组件,因此必须与故事代码共享同一份 React 实例。从源码看,addon-docs 的 webpack/vite 预设通过 resolvedReact 机制把 react、react-dom 与 @mdx-js/react 强制别名到项目根目录解析出的版本(项目未显式安装 React 时回退到 addon-docs 自带的版本):
// code/addons/docs/src/preset.ts (L220-L224)
export const resolvedReact = async (existing: any) => ({
react: existing?.react ?? resolvePackageDir('react'),
reactDom: existing?.reactDom ?? resolvePackageDir('react-dom'),
mdx: existing?.mdx ?? fileURLToPath(import.meta.resolve('@mdx-js/react')),
});
webpack 路径下这些别名会注入 resolve.alias(见 preset.ts);Vite 路径下则通过一个 storybook:package-deduplication 预置插件实现同样目的,并额外处理了 react-dom/server 的导出映射(preset.ts)。源码注释明确指出:多份 React/emotion 实例并存会导致文档组件渲染失败——这是使用 Vite 构建时遇到"文档页空白"类问题的首要排查方向。
三、DocsPage:零配置文档页
component 元数据是文档的数据源头
DocsPage 从多处汇聚信息,其中最核心的是故事元数据中的 component 字段(Storybook 5.2 引入)。Storybook 依据它提取组件的描述与 Props:
import { Button } from './Button';
export default {
title: 'Button',
component: Button,
};
页面由"文档块(doc blocks)"拼装
DocsPage 并非一个不可分割的黑盒,它由一组可复用的文档块组成。仓库中这些块的实现位于 blocks 目录(Title、Subtitle、Description、Primary、ArgsTable、Stories、Story、Source 等)。当你不需要默认页面时,可以在任意层级用 docs.page 参数替换:
null:移除文档页;- MDX 组件:改用 MDX 文档;
- 自定义 React 组件:用文档块自由重组。
全局替换(.storybook/preview.js):
import { addParameters } from '@storybook/react';
addParameters({ docs: { page: null } });
组件级替换(Button.stories.js):
import { Button } from './Button';
export default {
title: 'Demo/Button',
component: Button,
parameters: { docs: { page: null } },
};
故事级替换:
export const basic = () => <Button>Basic</Button>;
basic.parameters = { docs: { page: null } };
用文档块重组页面的示例(在 docs.page 中传入自定义组件):
import React from 'react';
import { ArgsTable, Description, Primary, Stories, Subtitle, Title } from '@storybook/addon-docs';
import { DocgenButton } from '../../components/DocgenButton';
export default {
title: 'Addons/Docs/stories docs blocks',
component: DocgenButton,
parameters: {
docs: {
page: () => (
<>
<Title />
<Subtitle />
<Description />
<Primary />
<ArgsTable />
<Stories />
</>
),
},
},
};
你可以在块之间插入自定义组件,也可以给块传参定制外观。
四、Props 表:从 component 到 ArgTypes
Storybook Docs 会基于 PropTypes 或 TypeScript 类型自动生成组件的 Props 表。要展示某组件的 Props 表,关键是填好故事元数据中的 component 字段(见上节示例)。
在 MDX 中,Props 表通过 ArgsTable 块使用:
import { ArgsTable } from '@storybook/addon-docs';
import { Button } from './Button';
# Button
<ArgsTable of={Button} />
从源码结构看,ArgsTable 的渲染逻辑(行级组件 ArgRow、可切换页签的 TabbedArgsTable 等)位于 ArgsTable 组件目录,它消费的正是由 docgen 提取出的 ArgTypes 数据结构;of={component} 与 story="name" 两种写法分别对应"按组件提取"和"按故事提取"两条数据链路。
五、MDX:Markdown 与文档组件的混合体
MDX 是一种用 Markdown 写文档、同时内嵌故事与 Props 表等文档组件的格式。要加载 MDX 文件,先把 .storybook/main.js 的 stories glob 扩展为包含 .mdx:
export default {
stories: ['../src/stories/**/*.stories.@(js|mdx)'],
};
然后就可以创建如下 MDX 文件:
import { Meta, Story, ArgsTable } from '@storybook/addon-docs';
import { Button } from './Button';
<Meta title='Button' component={Button} />
# Button
Some **markdown** description, or whatever you want.
<Story name='basic' height='400px'>
<Button>Label</Button>
</Story>
## ArgsTable
<ArgsTable of={Button} />
编译链路上,addon-docs 的构建预设为两种打包器分别挂了 MDX 处理器:webpack 下对 .mdx 文件(排除 *.stories.mdx)应用 mdx-loader 并追加 rehype-slug、rehype-external-links 等 rehype 插件(preset.ts);Vite 下则注入 mdxPlugin 并固定 MDX3 编译管线。Meta/Story 等块最终解析到 mdx.tsx 中对同一套文档块的封装。
六、内联故事与 iframe 故事
Storybook Docs 对 React 故事默认全部内联渲染(直接在文档页中以 React 组件形式呈现,无滚动条嵌套)。若希望改为 iframe 渲染,默认高度 60px,可用 docs.story.iframeHeight 调整高度,开关参数是 docs.story.inline。对全部故事生效时,更新 .storybook/preview.js:
export const parameters = { docs: { story: { inline: false } } };
补充两点:
- 原 React 文档文字中写的是
docs.stories.inline,但其配套示例与 DocsPage 参考文档 使用的实际参数名均为docs.story.inline,以参数对象{ docs: { story: { inline: false } } }为准; - 内联渲染依赖框架提供"把故事内容转成 React 可渲染结构"的能力(其他框架通过
prepareForInline参数实现,React 则是天然内联)。
七、TypeScript Props 提取:react-docgen vs react-docgen-typescript
如果你使用 TypeScript,Storybook 提供两种 Props 提取方案:react-docgen(默认)与 react-docgen-typescript。在 .storybook/main.js 中切换(或禁用):
export default {
typescript: {
// also valid 'react-docgen' | false
reactDocgen: 'react-docgen-typescript',
},
};
源码中的分派逻辑
React 渲染器在提取 ArgTypes 时,正是读取 typescript 预设来分派到不同提取器:
// code/renderers/react/src/preset.ts (L113-L138)
const { reactDocgen = 'react-docgen', reactDocgenTypescriptOptions } = typescriptOptions;
// If docgen is disabled, return null
if (reactDocgen === false) {
return null;
}
// ...
if (reactDocgen === 'react-docgen-typescript') {
argTypesData = await extractArgTypesFromDocgenTypescript({
componentFilePath: resolvedFilePath,
componentExportName,
reactDocgenTypescriptOptions,
});
} else {
// Default to 'react-docgen'
argTypesData = extractArgTypesFromDocgen({ componentFilePath, componentExportName });
}
三个取值语义清晰:默认 react-docgen(同步、基于 AST);react-docgen-typescript(异步、基于 TS 编译器);false 直接禁用 docgen,返回 null。
当选择 react-docgen-typescript 时,提取器会读取项目 tsconfig 并以一组默认解析器选项启动 parser,允许用户通过 reactDocgenTypescriptOptions 覆盖:
// code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts (L34-L54)
const defaultOptions: ReactDocgenTypescriptOptions = {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
// We *need* this set so that RDT returns default values in the same format as react-docgen
savePropValueAsString: true,
};
// ...
const parser = withCompilerOptions(
{ ...tsConfig, noErrorTruncation: true, strict: true },
mergedOptions
);
可以看到它默认排除 node_modules 中的属性、开启枚举字面量提取,并强制 savePropValueAsString 以保证默认值格式与 react-docgen 输出一致——这也是两种方案结果可比的前提。
两种方案的权衡(原文档结论)
两种方案都不完美,以下是原文档给出的完整对比:
react-docgen-typescript |
react-docgen |
|
|---|---|---|
| Features | Great. The analysis produces great results which gives the best props table experience. | OK. React-docgen produces basic results that are fine for most use cases. |
| Performance | Slow. It's doing a lot more work to produce those results, and may also have an inefficient implementation. | Blazing fast. Adding it to your project increases build time negligibly. |
| Bugs | Some. There are corner cases that are not handled properly, and are annoying for developers. | Some. There are corner cases that are not handled properly, and are annoying for developers. |
| SB docs | Good. Our prop tables have supported react-docgen-typescript results from the beginning, so it's relatively stable. |
OK. There are some obvious improvements to fully support react-docgen, and they're coming soon. |
性能是高频问题,原文档给出了一个随机项目的构建耗时量化(结果可能因项目而异):
| Docgen | Build time |
|---|---|
| react-docgen-typescript | 33s |
| react-docgen | 29s |
| none | 28s |
即 react-docgen-typescript 相比关闭 docgen 增加约 5 秒构建时间,换来的是更完整的类型信息(联合类型、泛型、TS 接口继承等)。
八、延伸阅读
围绕 React 文档主题,仓库内以下文档可继续深入:
- DocsPage 参考:文档块重组、
docs.page替换策略、docs.canvas.sourceState控制代码块默认展开/收起; - Props 表参考:ArgTypes 定制、Controls 列、各框架已知限制;
- MDX 参考 与 FAQ;
- 文档块实现源码:
Title、Primary、ArgsTable、Source等块的 React 实现; - React 渲染器预设 与 addon-docs 预设:构建链路、React 单例别名、MDX 编译配置的完整实现。
适用前提说明:本文配置示例基于当前仓库中
@storybook/addon-docs与@storybook/react的现有实现;原文档中的迁移提示面向 Storybook 5.3 配置格式变更,如需从旧版docs配置迁移,请参考仓库根目录的 MIGRATION.md。
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
