Storybook CSF 实战:为 Checkbox 组件编写「Unchecked」状态 Story(CSF 3 / Svelte CSF / CSF Next 全框架对照)
本文围绕 Storybook 官方文档代码片段 docs/_snippets/checkbox-story-csf.md 展开。该片段本身并非散文式教程,而是一份「可被文档站点多框架切换渲染」的对照式代码示例集:它用 16 段代码覆盖 Angular、Svelte、React、Vue、Web Components 等渲染器,演示在 Component Story Format(CSF)下为一个 Checkbox 组件定义名为Unchecked的默认状态 Story,并用args传入label: 'Unchecked'作为组件输入。通过逐段拆解这些示例,你将掌握 CSF 3 的对象式 Story、Svelte CSF 的defineMeta/Story写法、Web Components 的标签字符串引用,以及实验性 CSF Next 的preview.meta()API,并能把这些 Story 无缝嵌入 MDX 组件文档中渲染。
该代码片段在文档体系中的位置
在 Storybook 仓库中,docs/_snippets/目录存放的是所有教程页共用的「可切换代码片段」。它们由文档站点的CodeSnippets组件加载,并根据当前阅读者选择的框架、语言与写法选项卡,只渲染对应的那一段代码。
这一份checkbox-story-csf.md是 MDX 教程页 的主角之一。该页第 15 行起以Checkbox.mdx为例讲解「用 Markdown + JSX 写组件文档」,第 17 行引用 checkbox-story.md(展示 MDX 文件本体),第 21 行引用我们讨论的checkbox-story-csf.md,并说明它承载的是被 MDX 引用的、以 CSF 编写的Checkbox.stories.js|ts故事文件:
This MDX file references a story file,
Checkbox.stories.js|ts, that is written in Component Story Format (CSF).
换句话说:CSF 负责定义"组件有哪些状态",MDX 负责把状态编排成可读的文档。仓库中另一个片段 checkbox-story-grouped.md 展示了同系列示例如何通过title: 'Design System/Atoms/Checkbox'进行分组与层级命名,可一并对照阅读。
CSF 3:用「默认导出 + 具名导出」描述组件元数据与 Story
CSF 是官方推荐的故事编写格式,本质是一个基于 ES6 模块的开放标准(详见 docs/api/csf/index.mdx)。一个 CSF 故事文件由两部分构成:
- 默认导出(default export):描述组件元数据,至少包含
component字段(Addons 依赖它生成属性表、展示组件元信息),可选title、decorators、parameters等; - 具名导出(named exports):默认情况下每一个具名导出都是一个 Story 对象。
以最常见的CSF 3写法为例,下面是用 JavaScript 编写的通用版本(同一文件既可用于.js也可用于.jsx,覆盖 React 等 JSX 生态):
// Checkbox.stories.js|jsx —— CSF 3(common) import { Checkbox } from './Checkbox'; export default { component: Checkbox, }; export const Unchecked = { args: { label: 'Unchecked', }, };要点拆解:
export default { component: Checkbox }声明「本文件围绕 Checkbox 编写」,Story 将默认渲染该组件并把args作为输入分发给它;export const Unchecked = { args: {...} }声明一个名为Unchecked的 Story 对象。与 CSF 2 的函数式Unchecked.bind({})不同,CSF 3 中具名导出是对象,因此可以放心地用 JS 展开运算符复用其上的所有注解;args是 Storybook 6.0 起引入的「具名输入」。组件的外显差异(选中/未选中、主按钮/次按钮)通常只需不同的args就能表达,本例中的label: 'Unchecked'会作为属性传给组件;- 若未显式指定,Story 在侧边栏的显示名由具名导出经
startCase规则转换而来:Unchecked→Unchecked。官方建议具名导出一律使用 UpperCamelCase(详见 docs/api/csf/index.mdx 中导出标识符与显示名的映射表)。
TypeScript 版本:satisfies+StoryObj
在 TS/TSX 项目中,推荐用satisfies让编辑器同时做类型收窄与推断,并借助StoryObj获得自动补全:
// Checkbox.stories.ts|tsx —— CSF 3(common) // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. import type { Meta, StoryObj } from '@storybook/your-framework'; import { Checkbox } from './Checkbox'; const meta = { component: Checkbox, } satisfies Meta<typeof Checkbox>; export default meta; type Story = StoryObj<typeof meta>; export const Unchecked: Story = { args: { label: 'Unchecked', }, };注意此处相对 纯 JS 版 的两个差异:
import type { Meta, StoryObj } from '@storybook/your-framework'中的包名是占位符,实际使用时要替换为@storybook/react-vite、@storybook/nextjs、@storybook/vue3-vite等具体框架包(源码片段中以注释形式标注了这一点);- 用
const meta = {...} satisfies Meta<typeof Checkbox>约束元数据,再以type Story = StoryObj<typeof meta>让每个具名导出与组件的 props 类型自动对齐——写错属性名会在编译期直接报错。
Angular 版本:从组件类导入
Angular 渲染器同样遵循 CSF 3,区别在于组件来源与类型来自@storybook/angular:
// Checkbox.stories.ts —— CSF 3(angular) import type { Meta, StoryObj } from '@storybook/angular'; import { Checkbox } from './checkbox.component'; const meta: Meta<Checkbox> = { component: Checkbox, }; export default meta; type Story = StoryObj<Checkbox>; export const Unchecked: Story = { args: { label: 'Unchecked', }, };Angular 下Checkbox是从./checkbox.component导入的组件类,Meta<Checkbox>/StoryObj<Checkbox>直接以组件类型作为泛型参数,label会被编译为组件@Input()的输入值。
Svelte:两种写法并存
Svelte 是这套示例中唯一提供「两套范式」的渲染器,这与仓库教程页 docs/writing-stories/index.mdx 的描述一致:可以走标准 CSF(默认导出 + 具名导出),也可以使用 Svelte 生态惯用的@storybook/addon-svelte-csf。
标准 CSF 3(JS / TS)
// Checkbox.stories.js —— CSF 3(svelte) import Checkbox from './Checkbox.svelte'; export default { component: Checkbox, }; export const Unchecked = { args: { label: 'Unchecked', }, };TypeScript 版则按常见惯例引入框架包占位符@storybook/your-framework,实际使用时替换为svelte-vite或sveltekit(该提示写在片段源码注释中):
// Checkbox.stories.ts —— CSF 3(svelte) // Replace your-framework with svelte-vite or sveltekit import type { Meta, StoryObj } from '@storybook/your-framework'; import Checkbox from './Checkbox.svelte'; const meta = { component: Checkbox, } satisfies Meta<typeof Checkbox>; export default meta; type Story = StoryObj<typeof meta>; export const Unchecked: Story = { args: { label: 'Unchecked', }, };Svelte CSF:defineMeta+<Story>
标准 CSF 对 Svelte 社区而言并非唯一选择。官方教程页将其描述为社区驱动的平行方案:用defineMeta描述组件,用其返回的Story组件定义每个 Story。JS 与 TS 两种语言下的写法几乎一致:
<!-- Checkbox.stories.svelte —— Svelte CSF(js) --> <script module> import { defineMeta } from '@storybook/addon-svelte-csf'; import Checkbox from './Checkbox.svelte'; const { Story } = defineMeta({ component: Checkbox, }); </script> <Story name="Unchecked" args={{ label: 'Unchecked', }} /><!-- Checkbox.stories.svelte —— Svelte CSF(ts) --> <script module> import { defineMeta } from '@storybook/addon-svelte-csf'; import Checkbox from './Checkbox.svelte'; const { Story } = defineMeta({ component: Checkbox, }); </script> <Story name="Unchecked" args={{ label: 'Unchecked', }} />Svelte CSF 的关键差异在于「命名方式」:Story 的显示名不再由导出标识符推断,而是通过<Story>组件的name属性显式声明(侧边栏直接显示name的值)。args、decorators、parameters等注解同样以属性形式写在<Story>上(详见 docs/api/csf/index.mdx 中针对 Svelte 渲染器的说明)。还有一个易踩的坑记录在 docs/writing-stories/index.mdx 中:Svelte CSF 下不能用args传children,需要在<Story>开闭标签之间书写子内容,它会作为 Svelte 的childrensnippet prop 传入。
Web Components:直接用自定义元素标签名
Web Components 渲染器下,component字段不再引用框架组件类/对象,而是注册后的自定义元素标签字符串(此处为demo-checkbox),Storybook 会据此实例化对应 Custom Element:
// Checkbox.stories.js —— CSF 3(web-components) export default { component: 'demo-checkbox', }; export const Unchecked = { args: { label: 'Unchecked', }, };TypeScript 版本从@storybook/web-components-vite导入类型。由于没有可推导 props 的组件对象,Meta与StoryObj均为无泛型/宽泛形式:
// Checkbox.stories.ts —— CSF 3(web-components) import type { Meta, StoryObj } from '@storybook/web-components-vite'; const meta: Meta = { component: 'demo-checkbox', }; export default meta; type Story = StoryObj; export const Unchecked: Story = { args: { label: 'Unchecked', }, };与 React/Vue 等版本不同,Web Components 版不需要 import 组件文件——前提是标签已在项目中注册(例如由当前文档的组件库或 preview 的全局配置完成注册)。
CSF Next(实验性 🧪):preview.meta()与meta.story()
代码片段中与CSF 3并列的选项卡CSF Next 🧪展示了一套仍在实验期的 API,形态上是一套「无默认导出」的函数式写法。其固定套路是:
- 从项目的
.storybook/preview(即 preview 配置文件导出)导入默认的preview; - 调用
preview.meta({ component, ... })得到meta; - 调用
meta.story({ args, ... })声明 Story,并直接具名导出。
Angular、Web Components、React、Vue 四种渲染器都给出了对应变体,结构完全平行:
// Checkbox.stories.ts —— CSF Next(angular) import preview from '../.storybook/preview'; import { Checkbox } from './checkbox.component'; const meta = preview.meta({ component: Checkbox, }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });// Checkbox.stories.js —— CSF Next(web-components) import preview from '../.storybook/preview'; const meta = preview.meta({ component: 'demo-checkbox', }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });// Checkbox.stories.ts —— CSF Next(web-components) import preview from '../.storybook/preview'; const meta = preview.meta({ component: 'demo-checkbox', }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });// Checkbox.stories.ts|tsx —— CSF Next(react) import preview from '../.storybook/preview'; import { Checkbox } from './Checkbox'; const meta = preview.meta({ component: Checkbox, }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });// Checkbox.stories.js|jsx —— CSF Next(react) import preview from '../.storybook/preview'; import { Checkbox } from './Checkbox'; const meta = preview.meta({ component: Checkbox, }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });// Checkbox.stories.ts —— CSF Next(vue) import preview from '../.storybook/preview'; import Checkbox from './Checkbox.vue'; const meta = preview.meta({ component: Checkbox, }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });// Checkbox.stories.js —— CSF Next(vue) import preview from '../.storybook/preview'; import Checkbox from './Checkbox.vue'; const meta = preview.meta({ component: Checkbox, }); export const Unchecked = meta.story({ args: { label: 'Unchecked', }, });可见 CSF Next 的两处统一:其一,../.storybook/preview是相对于故事文件位置的导入路径(你的项目需把该路径调整为真实相对路径);其二,Angular 从./checkbox.component导组件类、React 从./Checkbox具名导入、Vue 从./Checkbox.vue默认导入——变化的只有组件来源,API 骨架完全一致。
片段中还保留了一段注释<!-- JS snippets still needed while providing both CSF 3 & Next -->,说明文档站点在同时提供 CSF 3 与 CSF Next 两种选项卡时,仍需为每个语言(JS/TS)分别维护片段——这也是本代码片段存在大量平行变体的直接原因。
组件的 story 定义与 CSF 3 中 Args 的作用
无论采用上面哪种范式,Unchecked这个 Story 的语义核心都在于args: { label: 'Unchecked' }。按 docs/api/csf/index.mdx 的说明,Args 自 SB 6.0 起成为 Story 的标准输入:
- 可被 addons 动态更新:
Controls、Actions等 addon 可以在界面上直接修改 args,让 Story 在渲染期间实时改变,这是"文档里也能交互"的基础; - 比硬编码渲染更可移植:写法本身不依赖某个具体 addon 或框架渲染器,故事文件可以跨工具复用;
- 无需自定义 render:如果 Story 只是「把 args 展开进组件」(本例即如此),CSF 3 会使用各渲染器内置的默认渲染函数,
Unchecked里无需再写任何render。
Story 命名与显示的换算规则
若未在 Story 对象上显式提供name,显示名按storyNameFromExport+ LodashstartCase规则转换(docs/api/csf/index.mdx 有完整映射表)。Unchecked这类 UpperCamelCase 单词会被原样展示;而some_custom_NAME这类命名则会被拆分为多词。需要特殊字符、保留字或稳定 Story ID 时,才建议改用name字段(Svelte CSF 的<Story name>即属此类)。
把 Unchecked Story 渲染进 MDX 组件文档
定义好Checkbox.stories.*后,接下来就是 docs/writing-docs/mdx.mdx 中Checkbox.mdx的编排逻辑。先通过import * as CheckboxStories from './Checkbox.stories'把整个故事文件的导出命名空间引入,然后用两个 Doc Block 完成两件事(完整片段见 checkbox-story.md):
<Meta of={CheckboxStories} /> # Checkbox A checkbox is a square box that can be activated or deactivated when ticked. Use checkboxes to select one or more options from a list of choices. <Canvas of={CheckboxStories.Unchecked} /><Meta of={CheckboxStories} />把文档页挂到 Checkbox 故事的相邻层级(sidebar 默认节点名为Docs,可通过name或title属性自定义)。MDX 教程页特别提醒:of应引用故事文件的完整导出集合(CheckboxStories),而不是组件本身,否则可能引发文档渲染问题;<Canvas of={CheckboxStories.Unchecked} />把Unchecked这个 Story 以内联 Canvas 形式嵌入正文——上文任何一种 CSF 变体产出的具名 Story 都能在此处被引用;- MDX 文档运行在 React 运行时中,而其中的 Story 仍按其自身渲染器(React/Vue/Angular/Svelte/Web Components 等)运行,这正是 CSF 故事文件能做到"一次定义、文档与组件生态互通"的原因。
如何选择范式
| 使用场景 | 推荐范式 | 依据 |
|---|---|---|
| React / Vue / Angular / 通用 TS 项目 | CSF 3 对象式 +satisfies | 官方推荐、类型安全、可展开复用 |
| Svelte 项目 | Svelte CSF(defineMeta/Story)或标准 CSF 3 | docs/writing-stories/index.mdx 同时介绍两种 |
| Web Components | CSF 3,component传自定义元素标签名 | 无需导入组件模块 |
| 尝鲜新 API、消除默认导出 | CSF Next(preview.meta()/meta.story()) | 片段中以 🧪 实验标记 |
同一份 Checkbox 故事文件之所以在 MDX 教程 中能以十余种形态呈现,正是得益于 CSF 的跨框架可移植性:掌握Unchecked这一个例子,也就掌握了在任何受支持渲染器中声明组件默认状态的标准套路。如需继续深入,可阅读 CSF 规范详解(含从 CSF 2 升级到 CSF 3 的 codemod 指引)、书写 Story 总览,以及 命名与层级组织 中配套的checkbox-story-grouped分组示例。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考