news 2026/9/10 4:16:48

用 Storybook 为组件编写第一支 Story:CSF 3 与 CSF Next 跨框架实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Storybook 为组件编写第一支 Story:CSF 3 与 CSF Next 跨框架实战指南

用 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文件模板,并理解componentargsrendertitle等核心字段的职责,以及新一代 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 文件由两部分组成:

  1. 默认导出(default export):即meta元数据,声明故事所属的组件(component)、可选的展示层级标题(title)等。注释中写得很清楚——"This default export determines where your story goes in the story list"(这个默认导出决定了你的故事在故事列表中的位置)。
  2. 命名导出(named exports):每一个导出的对象或函数就是一支 Story,Storybook 会在侧边栏中按名称列出它们。

在本文的代码片段中,你会看到两套书写范式:

  • CSF 3(当前主流)meta是普通对象,配合MetaStoryObj类型使用,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函数,返回一个包含componentssetuptemplate的渲染描述对象,把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-vitesveltekit

// 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-viteMetaStoryObj均为无类型参数形式:

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 写法同样保留自定义renderv-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

字段位置作用说明
componentmeta声明故事所属组件用于自动生成标题、推断 args/argTypes(控件),是侧边栏分组与 Docs 页的依据;Web Components 场景传自定义元素标签名
titlemeta(可选)手动指定故事层级位置,如'YourComponent''Components/Button'省略时 Storybook 按configure-story-loading规则从路径自动生成标题;HTML 片段中注释明确说明其可选性
argsStory组件在该 Story 下的输入参数会被自动映射为组件 props/输入;Basic片段中留空占位,实际按组件 props 填写,如{ label: 'Hello' }
renderStory(框架特定)自定义组件渲染方式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还可继续扩展argTypesdecoratorsparameters等字段,例如 docs/_snippets/component-story-custom-args-complex.md 展示了如何在meta.argTypes中定义带options的下拉控件。

六、运行并查看你的第一支 Story

写完stories文件后,在项目根目录启动 Storybook 开发服务器:

npm run storybook
yarn storybook
pnpm run storybook

Angular 项目(使用 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 4:15:39

嵌入式Linux下Modbus RTU开发实战:串口配置与协议实现

在嵌入式Linux上做工业通信&#xff0c;Modbus RTU几乎是一个绕不开的话题。无论是接一个温湿度传感器、采集一路模拟量&#xff0c;还是跟PLC、仪表对上数据&#xff0c;这套基于RS485的串口协议凭借简单、稳定、生态成熟&#xff0c;依然是现场设备接入的首选方式之一。 这篇…

作者头像 李华
网站建设 2026/9/10 4:15:09

嵌入式AI生成代码验证体系:从静态分析到实车路试的完整实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 4:12:07

CANN/GE更新图特征内存基址API

UpdateGraphFeatureMemoryBase 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTor…

作者头像 李华