首页
/ Storybook CSF 实战:为 Checkbox 组件编写「Unchecked」状态 Story(CSF 3 / Svelte CSF / CSF Next 全框架对照)

Storybook CSF 实战:为 Checkbox 组件编写「Unchecked」状态 Story(CSF 3 / Svelte CSF / CSF Next 全框架对照)

2026-09-07 09:26:42作者:伍希望

本文围绕 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.mdMDX 教程页 的主角之一。该页第 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 依赖它生成属性表、展示组件元信息),可选 titledecoratorsparameters 等;
  • 具名导出(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 规则转换而来:UncheckedUnchecked。官方建议具名导出一律使用 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 版 的两个差异:

  1. import type { Meta, StoryObj } from '@storybook/your-framework' 中的包名是占位符,实际使用时要替换为 @storybook/react-vite@storybook/nextjs@storybook/vue3-vite 等具体框架包(源码片段中以注释形式标注了这一点);
  2. 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-vitesveltekit(该提示写在片段源码注释中):

// 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 的值)。argsdecoratorsparameters 等注解同样以属性形式写在 <Story> 上(详见 docs/api/csf/index.mdx 中针对 Svelte 渲染器的说明)。还有一个易踩的坑记录在 docs/writing-stories/index.mdx 中:Svelte CSF 下不能用 argschildren,需要在 <Story> 开闭标签之间书写子内容,它会作为 Svelte 的 children snippet 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 的组件对象,MetaStoryObj 均为无泛型/宽泛形式:

// 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,形态上是一套「无默认导出」的函数式写法。其固定套路是:

  1. 从项目的 .storybook/preview(即 preview 配置文件导出)导入默认的 preview
  2. 调用 preview.meta({ component, ... }) 得到 meta
  3. 调用 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 动态更新ControlsActions 等 addon 可以在界面上直接修改 args,让 Story 在渲染期间实时改变,这是"文档里也能交互"的基础;
  • 比硬编码渲染更可移植:写法本身不依赖某个具体 addon 或框架渲染器,故事文件可以跨工具复用;
  • 无需自定义 render:如果 Story 只是「把 args 展开进组件」(本例即如此),CSF 3 会使用各渲染器内置的默认渲染函数,Unchecked 里无需再写任何 render

Story 命名与显示的换算规则

若未在 Story 对象上显式提供 name,显示名按 storyNameFromExport + Lodash startCase 规则转换(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.mdxCheckbox.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,可通过 nametitle 属性自定义)。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 分组示例。

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