用 Storybook 为组件编写第一支 Story:CSF 3 与 CSF Next 跨框架实战指南
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
本文以 Storybook 官方文档中docs/_snippets/your-component.md代码片段为骨架,系统讲解如何为你项目里的任意组件编写第一支 Story(故事)。无论你使用 React、Vue 3、Angular、Svelte、Solid、Preact、HTML 还是 Web Components,都能从中找到对应框架可直接复制运行的stories文件模板,并理解component、args、render、title等核心字段的职责,以及新一代 CSF Next(preview.meta()/meta.story())写法与经典 CSF 3 的差异。读完后,你将具备为任意技术栈的组件快速建立可浏览、可调试、可测试的 Storybook 工作台的能力。
一、前置概念:Story 与 CSF
Storybook 的核心思想是"隔离渲染":把一个 UI 组件放入一个独立的渲染环境(Canvas 预览 iframe)中,通过**Story(故事)**来描述组件在特定参数(args)下的一种渲染状态。编写 Story 所用的文件格式称为CSF(Component Story Format,组件故事格式),即用一个.stories.js、.stories.ts(或 Svelte 场景下的.stories.svelte)文件,通过模块导出约定来声明组件与它的多个状态。
一份 CSF 文件由两部分组成:
- 默认导出(default export):即
meta元数据,声明故事所属的组件(component)、可选的展示层级标题(title)等。注释中写得很清楚——"This default export determines where your story goes in the story list"(这个默认导出决定了你的故事在故事列表中的位置)。 - 命名导出(named exports):每一个导出的对象或函数就是一支 Story,Storybook 会在侧边栏中按名称列出它们。
在本文的代码片段中,你会看到两套书写范式:
- CSF 3(当前主流):
meta是普通对象,配合Meta、StoryObj类型使用,Story 是带args(参数)的对象。 - CSF Next(实验性 🧪):通过
preview.meta({...})声明元数据、meta.story({...})声明故事,元数据与故事在类型层面被强约束关联。
下面我们以官方代码片段中的YourComponent为例,按框架逐一展开。
二、开始之前:挑选组件与命名约定
官方入门页 docs/get-started/setup.mdx 给出的建议非常直接:从你的项目里挑一个简单组件(比如一个 Button),在它旁边新建一个*.stories.js/*.stories.ts/*.stories.svelte文件。Storybook 默认会按配置(main.js中的storiesglob)扫描这些文件,自动加载到侧边栏。随后运行 Storybook,即可在浏览器中看到渲染出来的组件——即使"看起来还有点怪"也是正常的,后续可以继续配置样式与运行环境。
代码片段中的YourComponent是一个占位组件名,实际使用时请替换为你的真实组件及其真实路径(如./Button、./your.component、./YourComponent.svelte等)。所有片段都遵循同一结构:先 import 组件,再定义meta默认导出,最后导出一支名为Basic的 Story。
三、CSF 3:为每个框架编写第一支 Story
3.1 React
React 的 JS 版本最简洁——meta只需声明component,Story 只需给出args,Storybook 会自动完成渲染与控件推断:
import { YourComponent } from './YourComponent'; //👇 This default export determines where your story goes in the story list export default { component: YourComponent, }; export const Basic = { args: { //👇 The args you need here will depend on your component }, };TypeScript 版本采用官方推荐的satisfies写法,让meta同时获得类型校验与字面量推断:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc. import type { Meta, StoryObj } from '@storybook/your-framework'; import { YourComponent } from './YourComponent'; //👇 This default export determines where your story goes in the story list const meta = { component: YourComponent, } satisfies Meta<typeof YourComponent>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { args: { //👇 The args you need here will depend on your component }, };注意:
@storybook/your-framework是占位包名,需要替换为你实际使用的框架包,如@storybook/react-vite、@storybook/nextjs、@storybook/nextjs-vite等。这在仓库的其它片段中同样适用(如 docs/_snippets/component-story-custom-args-complex.md 也沿用了该约定)。
3.2 Angular
Angular 的组件类需要作为Meta<YourComponent>的类型参数:
import type { Meta, StoryObj } from '@storybook/angular'; import { YourComponent } from './your.component'; //👇 This default export determines where your story goes in the story list const meta: Meta<YourComponent> = { component: YourComponent, }; export default meta; type Story = StoryObj<YourComponent>; export const Basic: Story = { args: { //👇 The args you need here will depend on your component }, };3.3 Vue 3
Vue 3 的 Story 需要自定义render函数,返回一个包含components、setup与template的渲染描述对象,把args通过v-bind="args"绑定到组件上:
import YourComponent from './YourComponent.vue'; //👇 This default export determines where your story goes in the story list export default { component: YourComponent, }; /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic = { render: (args) => ({ components: { YourComponent }, setup() { return { args }; }, template: '<YourComponent v-bind="args" />', }), args: { //👇 The args you need here will depend on your component }, };TypeScript 版本使用@storybook/vue3-vite的类型与satisfies写法,render结构完全一致:
import type { Meta, StoryObj } from '@storybook/vue3-vite'; import YourComponent from './YourComponent.vue'; const meta = { component: YourComponent, } satisfies Meta<typeof YourComponent>; //👇 This default export determines where your story goes in the story list export default meta; type Story = StoryObj<typeof meta>; /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic: Story = { render: (args) => ({ components: { YourComponent }, setup() { return { args }; }, template: '<YourComponent v-bind="args" />', }), args: { //👇 The args you need here will depend on your component }, };3.4 Svelte
Svelte 提供两套写法:Svelte CSF(.stories.svelte单文件,用defineMeta+<Story>组件声明)与标准CSF 3(.stories.js/.stories.ts)。
Svelte CSF 的 JS 版本(<script module>中的defineMeta返回Story组件,模板中通过args属性传参):
<script module> import { defineMeta } from '@storybook/addon-svelte-csf'; import YourComponent from './YourComponent.svelte'; //👇 This export determines where your story goes in the story list const { Story } = defineMeta({ component: YourComponent, }); </script> <Story name="Basic" args={{ /*👇 The args you need here will depend on your component */ }} />标准 CSF 3 的 JS 版本:
import YourComponent from './YourComponent.svelte'; //👇 This default export determines where your story goes in the story list export default { component: YourComponent, }; export const Basic = { args: { //👇 The args you need here will depend on your component }, };Svelte CSF 的 TypeScript 版本与 JS 版本几乎一致(defineMeta会基于 Svelte 组件推导 Props 类型):
<script module> import { defineMeta } from '@storybook/addon-svelte-csf'; import YourComponent from './YourComponent.svelte'; //👇 This export determines where your story goes in the story list const { Story } = defineMeta({ component: YourComponent, }); </script> <Story name="Basic" args={{ /*👇 The args you need here will depend on your component */ }} />CSF 3 的 TypeScript 版本同样需要把your-framework占位替换为svelte-vite或sveltekit:
// Replace your-framework with svelte-vite or sveltekit import type { Meta, StoryObj } from '@storybook/your-framework'; import YourComponent from './YourComponent.svelte'; //👇 This default export determines where your story goes in the story list const meta = { component: YourComponent, } satisfies Meta<typeof YourComponent>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { args: { //👇 The args you need here will depend on your component }, };3.5 HTML
HTML 场景没有组件类/组件实例,而是通过工厂函数createYourComponent把 args 渲染为 DOM。JS 版本需要显式给出title(或依赖自动标题生成),并借助框架特定的render函数:
import { createYourComponent } from './YourComponent'; // 👇 This default export determines where your story goes in the story list export default { /* 👇 The title prop is optional. * See https://storybook.js.org/docs/configure/#configure-story-loading * to learn how to generate automatic titles */ title: 'YourComponent', }; /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic = { render: (args) => createYourComponent(args), args: { // 👇 The args you need here will depend on your component }, };TypeScript 版本的类型层面使用ComponentProps作为Meta/StoryObj的类型参数:
import type { Meta, StoryObj } from '@storybook/html'; import { createYourComponent, ComponentProps } from './YourComponent'; //👇 This default export determines where your story goes in the story list const meta: Meta<ComponentProps> = { /* 👇 The title prop is optional. * See https://storybook.js.org/docs/configure/#configure-story-loading * to learn how to generate automatic titles */ title: 'YourComponent', }; export default meta; type Story = StoryObj<ComponentProps>; /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic: Story = { render: (args) => createYourComponent(args), args: { // 👇 The args you need here will depend on your component }, };3.6 Web Components
Web Components 的component字段传入的是自定义元素标签名(如'demo-your-component'),而非组件类。JS 版本:
// This default export determines where your story goes in the story list export default { component: 'demo-your-component', }; export const Basic = { args: { // 👇 The args you need here will depend on your component }, };TypeScript 版本使用@storybook/web-components-vite,Meta与StoryObj均为无类型参数形式:
import type { Meta, StoryObj } from '@storybook/web-components-vite'; // This default export determines where your story goes in the story list const meta: Meta = { component: 'demo-your-component', }; export default meta; type Story = StoryObj; export const Basic: Story = { args: { // 👇 The args you need here will depend on your component }, };3.7 Preact 与 Solid
Preact 使用 JSX 与/** @jsx h */指令,通过render返回<YourComponent {...args} />:
/** @jsx h */ import { h } from 'preact'; import { YourComponent } from './YourComponent'; //👇 This default export determines where your story goes in the story list export default { component: YourComponent, }; /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic = { render: (args) => <YourComponent {...args} />, args: { //👇 The args you need here will depend on your component }, };Solid 的 JS 版本与 React 极为相似(同样通过component+args声明):
import { YourComponent } from './YourComponent'; //👇 This default export determines where your story goes in the story list export default { component: YourComponent, }; export const Basic = { args: { //👇 The args you need here will depend on your component }, };Solid 的 TypeScript 版本使用storybook-solidjs-vite包:
import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { YourComponent } from './YourComponent'; //👇 This default export determines where your story goes in the story list const meta = { component: YourComponent, } satisfies Meta<typeof YourComponent>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { args: { //👇 The args you need here will depend on your component }, };四、CSF Next(实验性):用preview.meta()/meta.story()编写
CSF Next 是代码片段中标注 🧪(实验性)的另一种写法。它不再独立导出meta与命名的 Story 对象,而是先从.storybook/preview引入preview实例,再用preview.meta({...})得到meta,最后用meta.story({...})声明每一支 Story。它的优势在于:Story 与 meta 在类型上被绑定,args 的类型推断与校验更加严格。
以 React TypeScript 为例:
import preview from '../.storybook/preview'; import { YourComponent } from './YourComponent'; //👇 This default export determines where your story goes in the story list const meta = preview.meta({ component: YourComponent, }); export const Basic = meta.story({ args: { //👇 The args you need here will depend on your component }, });React JavaScript 版本结构一致:
import preview from '../.storybook/preview'; import { YourComponent } from './YourComponent'; const meta = preview.meta({ component: YourComponent, }); export const Basic = meta.story({ args: { //👇 The args you need here will depend on your component }, });Angular 的 CSF Next 写法(preview从../.storybook/preview引入):
import preview from '../.storybook/preview'; import { YourComponent } from './your.component'; //👇 This default export determines where your story goes in the story list const meta = preview.meta({ component: YourComponent, }); export const Basic = meta.story({ args: { //👇 The args you need here will depend on your component }, });Vue 3 的 CSF Next 写法同样保留自定义render(v-bind="args"模板绑定):
import preview from '../.storybook/preview'; import YourComponent from './YourComponent.vue'; //👇 This default export determines where your story goes in the story list const meta = preview.meta({ component: YourComponent, }); /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic = meta.story({ render: (args) => ({ components: { YourComponent }, setup() { return { args }; }, template: '<YourComponent v-bind="args" />', }), args: { //👇 The args you need here will depend on your component }, });以及对应的 JavaScript 版本:
import preview from '../.storybook/preview'; import YourComponent from './YourComponent.vue'; //👇 This default export determines where your story goes in the story list const meta = preview.meta({ component: YourComponent, }); /* *👇 Render functions are a framework specific feature to allow you control on how the component renders. * See https://storybook.js.org/docs/api/csf * to learn how to use render functions. */ export const Basic = meta.story({ render: (args) => ({ components: { YourComponent }, setup() { return { args }; }, template: '<YourComponent v-bind="args" />', }), args: { //👇 The args you need here will depend on your component }, });Web Components 的 CSF Next 写法(component仍是元素标签名):
import preview from '../.storybook/preview'; // 👇 This default export determines where your story goes in the story list const meta = preview.meta({ component: 'demo-your-component', }); export const Basic = meta.story({ args: { // 👇 The args you need here will depend on your component }, });import preview from '../.storybook/preview'; // 👇 This default export determines where your story goes in the story list const meta = preview.meta({ component: 'demo-your-component', }); export const Basic = meta.story({ args: { // 👇 The args you need here will depend on your component }, });源码实证:preview从哪来
CSF Next 中的preview来自项目的.storybook/preview.*文件。以本仓库为例,code/.storybook/preview.tsx 通过definePreview组装 addons、decorators、loaders、parameters 并默认导出:
export default definePreview({ addons: [ addonDocs(), addonThemes(), addonA11y(), addonTest(), addonPseudoStates(), templatePreview, ], decorators, loaders, tags: ['test', 'vitest'], parameters, });而各框架的 stories 则直接import preview from '../../../../../.storybook/preview.tsx'后调用preview.meta()与meta.story()。仓库内部真实案例可见 code/core/src/components/components/Button/Button.stories.tsx:
import preview from '../../../../../.storybook/preview.tsx'; import { Button } from './Button.tsx'; const meta = preview.meta({ id: 'button-component', title: 'Button', component: Button, args: { onClick: fn() }, }); export const Base = meta.story({ args: { ariaLabel: false, children: 'Button' }, });从源码结构看,preview.meta()/meta.story()是对项目级preview实例的链式封装,其底层仍然走 Storybook 的预览运行时(Preview 类 负责加载项目注解、初始化 StoryStore、按 storyId 加载并渲染 Story),因此 CSF Next 与 CSF 3 最终会汇入同一条渲染管线,二者可以按需选用。
五、核心字段速查:component、title、args 与 render
| 字段 | 位置 | 作用 | 说明 |
|---|---|---|---|
component | meta | 声明故事所属组件 | 用于自动生成标题、推断 args/argTypes(控件),是侧边栏分组与 Docs 页的依据;Web Components 场景传自定义元素标签名 |
title | meta(可选) | 手动指定故事层级位置,如'YourComponent'或'Components/Button' | 省略时 Storybook 按configure-story-loading规则从路径自动生成标题;HTML 片段中注释明确说明其可选性 |
args | Story | 组件在该 Story 下的输入参数 | 会被自动映射为组件 props/输入;Basic片段中留空占位,实际按组件 props 填写,如{ label: 'Hello' } |
render | Story(框架特定) | 自定义组件渲染方式 | Vue 3 需返回{ components, setup, template }描述对象;Preact 直接返回 JSX;HTML 调用工厂函数;React/Solid 等可由 Storybook 自动渲染,无需手写 |
需要说明:render是框架特定能力(片段注释原文:"Render functions are a framework specific feature to allow you control on how the component renders"),只有在需要控制渲染细节时才必须书写。CSF 3 下meta还可继续扩展argTypes、decorators、parameters等字段,例如 docs/_snippets/component-story-custom-args-complex.md 展示了如何在meta.argTypes中定义带options的下拉控件。
六、运行并查看你的第一支 Story
写完stories文件后,在项目根目录启动 Storybook 开发服务器:
npm run storybookyarn storybookpnpm run storybookAngular 项目(使用 builder 方式)则运行:
ng run my-project:storybook启动后在浏览器打开 Storybook 本地地址(默认http://localhost:6006),在左侧侧边栏中找到你的组件(层级由title或自动标题决定),点击Basic即可在 Canvas 中看到组件的隔离渲染效果。如果组件依赖主题、路由等外部上下文,需要通过装饰器(decorator)包裹渲染,相关模板可参考 docs/writing-stories/decorators.mdx(其内部引用的your-component-with-decorator.md系列片段与本文同源)。
七、进阶:这仅仅是个开始
第一支BasicStory 是 Storybook 工作流的入口,在此基础上你可以继续:
- 丰富 Story 变体:在同一个文件中继续导出
export const Dark: Story = { args: {...} }等多支 Story,覆盖组件的不同状态; - 配置控件与文档:通过
meta.argTypes定制 Controls 面板(下拉、开关、颜色选择器等),并配合自动生成的 Docs 页撰写组件说明; - 编写测试:为 Story 附加 play 函数或直接基于 Story 编写交互测试、快照测试,Storybook 官方仓库的 test-storybooks 目录有大量可参考的真实示例;
- 理解片段机制(面向文档贡献者):本文所有代码块都来自
docs/_snippets/your-component.md,该文件通过renderer(框架)、language(语言)、tabTitle(页签标题)等属性为不同框架/语言维护多套变体,并在文档中用<CodeSnippets path="your-component.md" />引用,具体约定见 docs/contribute/documentation/new-snippets.mdx。
八、总结
docs/_snippets/your-component.md用 8 个渲染器 ×(CSF 3 + CSF Next)的全套模板,回答了"如何为你的组件写第一支 Story"这一最基础的入门问题。其核心要点可以浓缩为三句话:默认导出meta决定故事归属与层级;命名导出即 Story,args描述输入状态;需要自定义渲染时用框架特定的render函数。掌握了这套骨架,再结合框架自身的类型系统(Meta/StoryObj/satisfies)与项目级preview配置,你就能在任何技术栈中快速为组件建立起可浏览、可交互、可测试的 Storybook 工作台。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考