首页
/ Storybook React 文档实战:用 @storybook/addon-docs 自动生成 DocsPage、Props 表与 MDX 长文档

Storybook React 文档实战:用 @storybook/addon-docs 自动生成 DocsPage、Props 表与 MDX 长文档

2026-09-06 10:29:29作者:薛曦旖Francesca

本篇技术指南基于 Storybook 仓库中的 React 框架文档 code/addons/docs/docs/frameworks/REACT.md,系统讲解如何为 React 项目接入 @storybook/addon-docs:安装与主配置、DocsPage 自动生成机制、基于 component 元数据的 Props 表、MDX 富文档写法、内联/iframe 故事渲染策略,以及 react-docgenreact-docgen-typescript 两种 docgen 方案的取舍。读完后你可以直接在 React 项目中复制配置得到"开箱即用"的组件文档,并能从源码层面理解每个参数背后的实现。

Storybook DocsPage 自动生成的文档页面

一、@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.jsaddons 列表中注册:

export default {
  // other settings
  addons: ['@storybook/addon-docs'];
};

安装完成后,你应该立即为所有故事获得基础的 DocsPage 文档,入口就是 Storybook UI 中的 Docs 标签。

底层细节:React 单例与包去重

Docs 的文档页与故事块(blocks)都是 React 组件,因此必须与故事代码共享同一份 React 实例。从源码看,addon-docs 的 webpack/vite 预设通过 resolvedReact 机制把 reactreact-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 目录(TitleSubtitleDescriptionPrimaryArgsTableStoriesStorySource 等)。当你不需要默认页面时,可以在任意层级用 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-slugrehype-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 文档主题,仓库内以下文档可继续深入:

适用前提说明:本文配置示例基于当前仓库中 @storybook/addon-docs@storybook/react 的现有实现;原文档中的迁移提示面向 Storybook 5.3 配置格式变更,如需从旧版 docs 配置迁移,请参考仓库根目录的 MIGRATION.md

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