☰
Storybook CSF 2 故事函数写法详解:六大渲染器的渲染描述与 CSF 3 迁移指南
2026/11/2 4:40:54 网站建设 项目流程

Storybook CSF 2 故事函数写法详解:六大渲染器的渲染描述与 CSF 3 迁移指南

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

本篇指南围绕 Storybook 官方 CSF(Component Story Format,组件故事格式)文档中csf-2-example-story这一示例展开,系统讲解CSF 2 时代"故事即渲染函数"在 Angular、React、Solid、Svelte、Vue 3、Web Components 六个主流渲染器下的标准写法与背后的类型契约,并以此为起点梳理向 CSF 3 故事对象迁移的完整路径(显式/默认 render、args 继承、自动标题与 codemod)。读完你将能读懂并改写仓库中任一套框架的.stories文件,并掌握两种 CSF 版本之间的逐行换算方法。

背景:CSF 故事文件的骨架

CSF 是 Storybook 推荐的编写故事方式,它是一个基于 ES6 模块、可脱离 Storybook 移植的开放标准。每个故事文件由两部分组成(详见 docs/api/csf/index.mdx):

  • 默认导出(default export):描述组件元数据,核心字段是component(必填,供 addon 自动生成 prop 表格、展示组件元数据)与title(可选但需全局唯一,决定故事在导航层级中的位置)。
  • 具名导出(named exports):文件中的每个具名导出默认代表一个故事。

命名导出转换为界面显示名时,Storybook 会借助 LodashstartCase与storyNameFromExport处理,因此官方推荐具名导出统一以大写字母开头(如Primary、Basic),并允许通过includeStories/excludeStories把非故事导出(mock 数据等)排除在侧边栏之外。

CSF 2 的故事本质:命名导出是"渲染函数"

CSF 2(对应 Storybook 6.x 主流写法)约定:故事就是一个接收args、返回组件实例的函数,而args、parameters、decorators等配置则以函数静态属性的形式挂在它身上。这也是迁移到 CSF 3 时最大的心智差异点(对象 vs 函数 + 函数上的注解)。

下文csf-2-example-story展示的正是"故事函数本体"的最小形态——为了让读者聚焦渲染写法,示例用// Other imports and story implementation隐去了default export、组件 import 与 args 注解(带完整默认导出的同系列示例见 docs/_snippets/csf-2-example-starter.md,本文末尾会展开对照)。

React:JSX 直出 +ComponentStory类型

// CSF 2 - Button.stories.js|jsx (react) // Other imports and story implementation export const Basic = (args) => <Button {...args} />;
// CSF 2 - Button.stories.ts|tsx (react) // Other imports and story implementation export const Basic: ComponentStory<typeof Button> = (args) => <Button {...args} />;

JS 版本里Basic就是一个把args展开进<Button>的函数;TS 版本则用各 React 系框架(react-vite、nextjs、react-webpack5等)从@storybook/<framework>导出的ComponentStory<typeof Button>标注故事类型,让args、argsTypes获得组件 props 的完整类型推导。

Solid:与 React 同构,import 来源不同

// CSF 2 - Button.stories.js (solid) // Other imports and story implementation export const Basic = (args) => <Button {...args} />;
// CSF 2 - Button.stories.ts|tsx (solid) // Other imports and story implementation export const Basic: ComponentStory<typeof Button> = (args) => <Button {...args} />;

Solid 的故事函数形态与 React 一致,TS 类型的ComponentStory/ComponentMeta需要从 Solid 自身的框架包(如storybook-solidjs-vite)导入——这一点在完整示例 csf-2-example-starter.md 中体现得很清楚。

Angular:返回组件配置对象

// CSF 2 - Button.stories.ts (angular) // Other imports and story implementation export const Basic: Story = (args) => ({ props: args, });

Angular 的故事函数不再直接写 JSX,而是返回一个组件配置描述对象({ props: args }),由@storybook/angular的Story类型约束。component已在默认导出中声明,因此这里只需把args通过props注入到组件实例上。

Svelte:Component+props描述对象

// CSF 2 - Button.stories.js (svelte) // Other imports and story implementation export const Basic = (args) => ({ Component: Button, props: args, });
// CSF 2 - Button.stories.ts (svelte) // Other imports and story implementation export const Basic: StoryFn<typeof Button> = (args) => ({ Component: Button, props: args, });

Svelte 渲染器同样接收"渲染描述对象",用Component指向要渲染的组件、props透传 args。注意其 TS 命名是StoryFn<typeof Button>(从@storybook/sveltekit或svelte-vite引入),而非 React 系的ComponentStory——这是各渲染器类型导出差异的典型例证。

Vue 3:setup+ 模板模板引用

// CSF 2 - Button.stories.js (vue) // Other imports and story implementation export const Basic = (args) => ({ components: { Button }, setup() { return { args }; }, template: '<Button v-bind="args" />', });
// CSF 2 - Button.stories.ts (vue) // Other imports and story implementation export const Basic: StoryFn<typeof Button> = (args) => ({ components: { Button }, setup() { return { args }; }, template: '<Button v-bind="args" />', });

Vue 故事函数返回的是一个运行时组件定义:先在components注册组件,用setup()把args暴露给模板,再以<Button v-bind="args" />完成 props 绑定。TS 版本同样使用StoryFn<typeof Button>(如来自@storybook/vue3-vite)。

Web Components:基于 Lit 的模板函数

// CSF 2 - Button.stories.js (web-components) // Other imports and story implementation export const Basic = ({ primary, size, label }) => html`<custom-button ?primary=${primary} size=${size} label=${label}></custom-button>`;
// CSF 2 - Button.stories.ts (web-components) // Other imports and story implementation export const Basic: Story = ({ primary, backgroundColor, size, label }) => html`<custom-button ?primary=${primary} size=${size} label=${label}></custom-button>`;

Web Components 渲染器依赖lit的html模板标签。这里演示了解构接收args并逐项映射的写法:?primary=为布尔属性绑定(存在即真),size=、label=为普通字符串属性绑定。对比 JS/TS 变体可发现,TS 版Story类型约束了参数对象形状,并显式解构了四个字段。不同版本间字段不一致(TS 多出backgroundColor)正说明:CSF 2 的函数形参完全由你决定,组件在函数体内被如何组装也由你负责。

迁移:从"故事函数"到"故事对象"

csf-2-example-story在官方文档中并不是孤立存在的——它位于 docs/api/csf/index.mdx 的 "Upgrading from CSF 2 to CSF 3" 章节,其正文语序是:"我们先看一个简单的 CSF 2 故事函数……再把它改写成带显式render的故事对象"。因此正确理解这段示例的方式是把它作为迁移起点。

迁移要素一:render 函数外置

CSF 3 把命名导出从函数改为对象,渲染逻辑放入对象的render字段。上面的Basic函数可以被原样搬进render:

// CSF 3 - 显式 render(react 等 JSX 渲染器) // Other imports and story implementation export const Basic = { render: (args) => <Button {...args} />, };
// CSF 3 - 显式 render(vue 渲染器,TS) // Other imports and story implementation export const Basic: Story = { render: (args) => ({ components: { Button }, setup() { return { args }; }, template: '<Button v-bind="args" />', }), };

Svelte / Angular / Web Components 同理,只需把原有的渲染函数整体放进render字段(完整代码可对照 docs/_snippets/csf-3-example-render.md)。换言之,CSF 2 的"故事函数体"就是 CSF 3 中render的天然素材,二者是逐字对应的。

迁移要素二:利用各渲染器的默认 render

官方文档同时指出一个关键洞察:CSF 2 里大量故事函数是雷同的——"取出默认导出声明的组件,把 args 展开进去"。有趣的不是这个函数本身,而是传入的 args(见 docs/api/csf/index.mdx)。CSF 3 为每个渲染器都内置了默认 render 函数,因此最常见的"展开 args 渲染组件"场景连render都可以省略:

// CSF 3 - 什么都不用写(react) // Other imports and story implementation export const Basic = {};
// CSF 3 - 完整 starter(react, TS) import type { Meta, StoryObj } from '@storybook/your-framework'; import { Button } from './Button'; const meta = { component: Button, } satisfies Meta<typeof Button>; export default meta; type Story = StoryObj<typeof meta>; export const Basic: Story = { args: { primary: true } };

这正是示例片段标题csf-2-example-story(故事函数本身)与 csf-3-example-default-render(空对象{})之间的鲜明对比:一份故事文件里最有价值的信息是 args 与参数语义,而非渲染样板。

迁移要素三:注解继承问题与对象展开

CSF 2 中若要基于已有故事派生新故事,只能Primary.bind({}):

// CSF 2 - Button.stories.js|jsx|ts|tsx (common) export const PrimaryOnDark = Primary.bind({}); PrimaryOnDark.args = Primary.args; PrimaryOnDark.parameters = { background: { default: 'dark' } };

因为.bind({})只复制函数体,不会复制挂在函数上的args、parameters等注解,必须手动重新赋值(见 csf-2-example-primary-dark-story.md)。CSF 3 的故事是普通对象,可直接用对象展开无损继承全部注解:

// CSF 3 - 对象展开携带全部注解 (common) export const PrimaryOnDark = { ...Primary, parameters: { background: { default: 'dark' } }, };

迁移要素四:title 自动生成

CSF 2 必须在默认导出中手写层级标题(见 csf-2-example-title.md):

// CSF 2 - Button.stories.js|jsx|ts|tsx (common) export default { title: 'components/Button', component: Button, };

CSF 3 中 title 变为可选项,未指定时按磁盘上的文件路径推断(见 docs/_snippets/csf-3-example-auto-title.md),如需控制排序仍可显式声明。

一键迁移:codemod

Storybook 官方为升级提供 codemod(命令入口见 docs/api/csf/index.mdx),可在迁移前对存量 CSF 2 文件批量执行转换,把函数式命名导出改写为对象式。需要说明的是:codemod 处理的是结构迁移,渲染差异(如是否省略render)建议迁移后按上述"默认 render"原则手工精简。

两张对照速查表

渲染层对应关系(以Basic为例):

渲染器CSF 2 故事函数CSF 3 迁移结果
React / Solid(args) => <Button {...args} />{ render: (args) => <Button {...args} /> },或省略 render 直接{ args }
Angular(args) => ({ props: args })render内返回{ props: args }
Svelte(args) => ({ Component: Button, props: args })render内返回同一描述对象
Vue 3(args) => ({ components, setup, template })render内返回同一运行时组件定义
Web Components({ primary, size, label }) => html\...`|render` 内返回同一 lit 模板

配置挂载层对应关系:

能力CSF 2CSF 3
故事形态具名导出为函数具名导出为对象
注解挂载函数静态属性(Primary.args = ...)对象字段(args: { ... })
复用派生Primary.bind({})+ 手工重挂注解{ ...Primary }对象展开
渲染控制函数体即渲染可选的render字段 / 渲染器默认 render
标题默认导出必填title可选,可按文件路径自动推断

从函数式故事到可组合的故事资产

通读本示例及其配套片段后可以总结出贯穿 CSF 演进的一条主线:CSF 2 用函数把"组件 + args + 渲染方式"耦合在一起,故事只能复制、难以组合;CSF 3 则把三者解耦为component(默认导出)、args(数据)、render(可选渲染策略),使故事成为可展开、可继承、可被文档与测试工具直接消费的纯数据对象。

更深一层的证据在示例的 TS 类型中:ComponentStory、StoryFn、Story这些逐渲染器差异化的命名,到 CSF 3 时代被统一收敛为Meta/StoryObj(参见 csf-3-example-starter.md 中各框架均从各自包导入同一对类型),配合satisfies关键字实现组件 props 到 args 的精确类型约束——这也是升级迁移中 TS 报错最集中的区域,建议逐文件按"先类型后渲染"的顺序处理。

进一步阅读

  • docs/api/csf/index.mdx:CSF 规范全文,含命名导出转换规则、name字段、Args、play 函数、自定义 render 与includeStories/excludeStories等完整话题;
  • docs/_snippets/csf-2-example-starter.md:本示例的完整文件形态(含 default export 与 args 注解);
  • docs/_snippets/csf-2-example-primary-dark-story.md 与 docs/_snippets/csf-2-example-title.md:CSF 2 侧的故事派生与标题声明;
  • docs/_snippets/csf-3-example-render.md、docs/_snippets/csf-3-example-default-render.md、docs/_snippets/csf-3-example-starter.md:与本文逐渲染器对照的 CSF 3 版本;
  • 编写故事的一般性指南见 docs/writing-stories。

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询