Storybook Docs 配方实战指南:CSF 与 MDX 组合模式、文档页定制与关键参数详解
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
本文基于 Storybook 仓库中 Docs 插件自带的官方配方文档 recipes.md,系统讲解在 Storybook Docs 体系下如何组合 CSF(Component Story Format)与 MDX 两种故事描述机制,以及如何通过docs参数族(docs.disable、docs.container、docs.source.transform、viewMode等)定制文档页的呈现方式。读完本文,你可以为项目选择合适的故事/文档组织模式,并掌握文档页级别的描述提取、源码展示转换与容器覆盖等进阶配置。
Docs 插件的两套基础机制
Docs 插件 由两个基础机制构成:DocsPage(零配置自动生成文档页)和 MDX(用富文本语法书写文档与故事)。核心问题在于:它们各自适合什么场景,又该如何组合?官方配方文档给出的答案是——存在多种合法组合,且每种组合有明确的适用边界。
配方一:CSF + DocsPage(推荐主用法)
Component Story Format 是一种便捷、可移植的故事写法,而 DocsPage 是零配置即可为 CSF 故事生成富文档页面的机制。两者结合是 Storybook Docs 的首要使用场景:你只需按标准 CSF 编写.stories.js文件,文档页(包含 Canvas、Args 表格、Source 代码块、故事列表)即自动生成,无需任何额外配置。
如果你想在 Storybook 中穿插长篇文档(例如在故事列表开头放置一页设计系统介绍和安装说明),官方建议单独编写 Documentation-only 的 MDX 文件,而不是硬塞进组件故事文件。
配方二:纯 MDX Stories
MDX 是 CSF 之外的另一种故事语法,允许把故事和文档写在同一个文件里。配方文档明确指出:凡是 CSF 能做的事,MDX 都能做,并且两者在构建层面暴露的是相同的模块接口,文件可以互相替换。一些团队选择用 MDX 编写全部 Storybook 内容。
组合模式:CSF 故事 + MDX 文档
如果你希望故事逻辑用 CSF 写、文档用 MDX 写,标准做法是让 MDX 文件成为该组件的真正“故事文件”。
Button.stories.js
import React from 'react'; import { Button } from './Button'; // 注意:不再有 default export,因为 `Button.stories.mdx` 现在是 `Button` 的故事文件 // // export default { // title: 'Demo/Button', // component: Button, // }; export const basic = () => <Button>Basic</Button>; basic.parameters = { foo: 'bar', };Button.stories.mdx
import { Meta, Story } from '@storybook/addon-docs'; import * as stories from './Button.stories.js'; import { Button } from './Button'; import { SomeComponent } from 'path/to/SomeComponent'; <Meta title="Demo/Button" component={Button} /> # Button 我可以用从 CSF 导入的函数来定义故事: <Story story={stories.basic} /> 并且可以在这个文件中嵌入任意的 markdown 与 JSX。 <SomeComponent prop1="val1" />这套组合的工作机制如下:
- 故事定义在 CSF 文件中,但由于同名的
Button.stories.mdx存在,CSF 文件不再被直接注册为独立的故事列表(原文档以includeStories: []机制解释这一点),故事实际经由 MDX 中的<Story story={}>块呈现; - CSF 中命名导出的故事保留其故事级 decorators、parameters、args 标注,且
<Story story={}>构造会尊重这些标注(例如basic.parameters中定义的foo: 'bar'仍然生效); - 原本挂在
Button.stories.jsdefault export 上的组件级 decorators、parameters 等,不会自动继承,如需要必须手动拷贝到 MDX 的<Meta>中。
组合模式:CSF + 任意 MDX
官方推荐上面的“CSF + MDX Docs”作为标注 CSF 故事的最优方式,但如果只是想引用任意 markdown 文件作为文档页,还有第二种选择。
Button.mdx
import { Story } from '@storybook/addon-docs'; import { SomeComponent } from 'somewhere'; # Button 我可以嵌入一个已有的故事(但不能定义新故事,因为这个文件里不应该有 `Meta`): <Story id="some--id" /> 并且可以嵌入任意 markdown 与 JSX。 <SomeComponent prop1="val1" />Button.stories.js
import React from 'react'; import { Button } from './Button'; import mdx from './Button.mdx'; export default { title: 'Demo/Button', parameters: { docs: { page: mdx, }, }, component: Button, }; export const basic = () => <Button>Basic</Button>;这里有一个与上文截然不同的关键点:MDX 文件后缀是.mdx而非.stories.mdx。这个区别意味着该文件走的是默认 MDX loader,而不是 Storybook 的 CSF loader,由此带来三条约束:
- 文件中不应提供
Meta声明; - 只能引用已有故事(
<Story id="...">),不能定义新故事(不能用<Story name="...">); - 文档以 MDX 的 default export 形式导出,并通过
docs.page参数挂载,而不是像 CSF 那样作为 default export 参数的一部分。
这一点在插件的类型定义中有直接印证:docs.page被声明为“用你自己的组件替换 Storybook 默认文档模板”的选项,类型为ComponentType,见 types.ts。
混合 storiesOf 与 CSF/MDX
如果项目中还有一批使用storiesOf旧 API 的故事,或者存在只能用storiesOf实现的故事(例如动态生成的故事),需要与 CSF/MDX 共存。configure的第一个参数可以是require.context返回的req、req数组,或者一个 loader 函数;loader 函数要么返回 null,要么返回一个“全部包含 default export”的模块导出数组,configure依据 default export 来识别并加载 CSF/MDX 文件。
一个假设“storiesOf 文件都不含 default export”的朴素实现如下:
const loadFn = () => { const req = require.context('../src', true, /\.stories\.js$/); return req .keys() .map((fname) => req(fname)) .filter((exp) => !!exp.default); }; configure(loadFn, module);官方说明:他们可以把这个启发式逻辑内置进 Storybook,但无法假设你的storiesOf文件没有 default export。如果确实有,需要用别的方式(比如按文件名)过滤。若不过滤,会看到显式报错:
"Loader function passed to 'configure' should return void or an array of module exports that all contain a 'default' export"
官方特意让这个报错显式化,以提醒你正在混合storiesOf与 CSF/MDX。
从 notes/info 插件迁移:自定义描述提取
如果项目此前使用 notes/info 插件给每个组件挂了notes参数(markdown 文本),迁移到 Docs 插件时可以自定义docs.extractComponentDescription参数,把notes内容提取到文档页顶部的 Description 区域。假设你希望notes显示在Description插槽顶部,可以在.storybook/preview.js中加入:
import { addParameters } from '@storybook/preview-api'; addParameters({ docs: { extractComponentDescription: (component, { notes }) => { if (notes) { return typeof notes === 'string' ? notes : notes.markdown || notes.text; } return null; }, }, });从源码看,Description 块的渲染逻辑正是调用这个可插拔函数:在 Description.tsx 中,组件描述和故事描述均通过parameters.docs?.extractComponentDescription?.(component, { ... })动态计算,参数为可选链调用,未提供时回退到默认行为。docs preset 的默认extractComponentDescription从组件源码中提取 JSDoc 注释作为描述,并忽略第二个参数(当前选中故事的故事 parameters);而上例则相反——忽略注释、改用该故事的notes参数。
仓库中自带的文档故事也演示了最简单的形式,见 extract-description.stories.ts(直接extractComponentDescription: () => 'component description'返回固定字符串)。
导出纯文档站点
Storybook 的 UI 是“组件开发工作台”,而 Docs 是“文档展示橱窗”。开发时两种模式并排查看很有用;但导出静态站点时,可能希望只保留文档页以减少冗余。为此 Storybook 提供了 CLI 标志:
yarn storybook build --docs注意:原始配方文档写作于 Storybook 5.2 时期,当时该标志被标记为实验特性(5.3 中行为可能不受 semver 约束地变化)。在当前仓库所处的版本线中,它已成为 docs 构建的常规选项,适用前提是你的文档页由 docs 插件(DocsPage 或 MDX)驱动。
禁用文档中的故事:docs.disable
有两类场景需要把某些故事从文档页中排除。
DocsPage 场景
故事用 CSF 定义、用 DocsPage 渲染文档,但希望排除部分故事以减少页面噪音:
export const foo = () => <Button>foo</Button>; foo.parameters = { docs: { disable: true } };MDX 场景
在单个 MDX 文件中并排写文档与故事,希望故事出现在 Canvas 中但不进入文档页(相当于“CSF stories with MDX docs”的纯 MDX 版本):
<Story name="foo" parameters={{ docs: { disable: true } }}> <Button>foo</Button> </Story>参数类型上,docs.disable被声明为boolean,语义是“移除该插件面板并禁用其行为”,见 types.ts。
控制故事的视图模式:viewMode
Storybook 默认的导航行为是保持当前视图模式:用户处于 docs 模式时点击另一个故事,仍以 docs 模式打开;处于 story 模式(UI 中的 canvas)时,则保持 story 模式(例外:docs-only 页面永远以 docs 模式显示)。
基于用户反馈,可以用viewMode故事参数控制单个故事进入时强制切换的视图模式。以下示例保证导航到该故事时视图模式重置为 story:
export const Foo = () => <Component />; Foo.parameters = { // 用户导航到该故事时,始终把视图模式重置为 "story" viewMode: 'story', };也可以在.storybook/preview.js中全局生效:
// 用户导航时始终把视图模式重置为 "docs" export const parameters = { viewMode: 'docs', };调整 Docs 标签页顺序:previewTabs
用previewTabs故事参数可以配置 preview 区域的标签页顺序。把Docs标签排到最前面:
export const Foo = () => <Component />; Foo.parameters = { previewTabs: { 'storybook/docs/panel': { index: -1 } }, };同样可全局写在.storybook/preview.js中。
定制源码展示:docs.source.code 与 docs.source.transform
从 SB 6.0 起,定制 Docs 源码展示有两条互补的路径:
- 覆盖
docs.source.code:Source 块直接渲染你提供的字符串,适合故事级的精确控制:
const Example = () => <Button />; Example.parameters = { docs: { source: { code: 'some arbitrary string' } }, };- 提供
docs.source.transform函数:适合全局格式化。以下示例全局剥掉“返回字符串的箭头函数”外壳(() => \...``):
const SOURCE_REGEX = /^\(\) => `(.*)`$/; export const parameters = { docs: { source: { transform: (src, storyContext) => { const match = SOURCE_REGEX.exec(src); return match ? match[1] : src; }, }, }, };插件的源类型定义明确了 transform 的签名:(code: string, storyContext: any) => string | Promise<string>,即允许同步或异步转换,见 types.ts。同一配置块中还支持type('auto' | 'code' | 'dynamic',默认'auto')、format(true/'dedent'或任意 prettier parser 名)、language等选项,详见 types.ts。两种方法互补:前者适合故事级覆盖,后者适合全局格式化处理。
覆盖文档容器:docs.container
当你想给 MDX 页面包一层 wrapper 或注入 React context(例如styled-components的ThemeProvider)时需要注意:decorator 只作用于故事,而 MDX 中<Story>块之外的任意 JSX 不受 decorator 影响,此时必须使用docs.container参数。container是 Docs 体系中与 decorator 最接近的概念——一个包裹在被渲染页面外围的元素。
官方示例:给页面加一圈红色实线边框。它复用了 Storybook 默认的页面容器(负责搭建各类 context 与内部机制),然后在该容器与页面内容之间插入自定义逻辑:
import { Meta, DocsContainer } from '@storybook/addon-docs'; <Meta title="Addons/Docs/container-override" parameters={{ docs: { container: ({ children, context }) => ( <DocsContainer context={context}> <div style={{ border: '5px solid red' }}>{children}</div> </DocsContainer> ), }, }} /> # Title 文件的其余部分...styled-components主题场景下的典型用法:
import { Meta, DocsContainer } from '@storybook/addon-docs'; import { ThemeProvider } from 'styled-components'; import { theme } from '../path/to/theme'; <Meta title="Addons/Docs/container-override" parameters={{ docs: { container: ({ children, context }) => ( <DocsContainer context={context}> <ThemeProvider theme={theme}> {children} </ThemeProvider> </DocsContainer> ), }, }} /> # Title 文件的其余部分...实现层面,自定义容器的类型为ComponentType<DocsContainerProps>,见 types.ts;默认容器实现位于 DocsContainer.tsx。自定义container的推荐模式是包裹默认DocsContainer,以保留其提供的上下文与内部逻辑,而不是完全替换它。
为单个故事添加描述:docs.description.story
在docs.description参数中加入story字段,即可为单个故事添加描述(支持 markdown 语法):
const Example = () => <Button />; Example.parameters = { docs: { description: { story: 'Individual story description, may contain `markdown` markup', }, }, };配方文档还提到存在第三方 webpack loader(story-description-loader)可从 JSDoc 注释中提取描述,属于原文档时代的生态补充,当前使用时需自行确认其对新版本 Storybook 的兼容性。
延伸阅读
- 参考文档(本仓库内):README / DocsPage / MDX / FAQ / Theming / Props 表格
- 参数类型定义:code/addons/docs/src/types.ts(
DocsParameters.docs全量字段:argTypes、canvas、codePanel、controls、container、description、disable、page、source、story、stories、subtitle、lang、theme、title、toc) - 默认描述提取与自定义示例:Description.tsx / extract-description.stories.ts
- 各框架的 Docs 文档见 code/renderers/react、code/renderers/vue3 等框架渲染器目录下的配套说明
以上配方均直接取自仓库中的官方文档 code/addons/docs/docs/recipes.md,其中的参数签名与行为可通过 code/addons/docs/src/types.ts 的类型定义和 code/addons/docs/src/blocks/blocks 下的块实现逐一验证。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考