Storybook 组件故事的极简范式:用 Args 编排点击事件的全框架写法(CSF 3 与 CSF Next)
在 Storybook 的 Component Story Format(CSF) 中,"Args story inputs" 这一章用同一个"记录点击事件的按钮"(Button)讲述了故事写法的三段式演进——从手写 render 并硬编码 action(),到"Args + 显式 render",再到本文主角 button-story-click-handler-simplificated.md 所代表的纯 Args 极简形态。本文以该代码片段为骨架,逐一拆解其在 Angular、React、Solid、Svelte、Vue 与 Web Components 六类渲染器下的写法,并结合仓库文档与源码级机制解释"为什么可以省略 render、省略 action()",帮助你掌握 Storybook 中最简洁、最可移植、可被 Controls 实时编辑的故事编写范式。
一、这段代码片段在讲什么:三段式演进的终点
该片段作为 <CodeSnippets path="button-story-click-handler-simplificated.md" /> 被引用在 docs/api/csf/index.mdx 的 "Args story inputs" 小节。官方文档在引出它之前,先展示了同一个按钮故事的两种更"啰嗦"的写法:
- button-story-click-handler.md:不使用 Args,用
render硬编码组件属性,并在渲染函数内部直接调用action('clicked')生成回调,例如 React 的render: () => <Button label="Hello" onClick={action('clicked')} />; - button-story-click-handler-args.md:开始引入 Args,把
label、onClick挪进args,再用render: ({ label, onClick }) => <Button .../>消费它们; - button-story-click-handler-simplificated.md(本文主体):连
render都一并省去,故事里只剩一个(甚至为空的)args对象,某些渲染器仅需在meta里补充一个空的argTypes.onClick。
文档对这段演进给出的结论原话是:"Not only are these versions shorter and more accessible to write than their no-args counterparts, but they are also more portable since the code doesn't depend on the actions feature specifically."——即这种写法更短、更易上手,且不依赖 Actions 特性本身,因此更可移植。
二、为什么能省到只剩 args:两大 CSF 3 机制支撑
极简写法之所以成立,依赖 CSF 3 的两个核心机制,二者在 docs/api/csf/index.mdx 均有系统讲解。
1. 默认渲染函数(Default render functions)自动把 Args 铺进组件
CSF 2 中,story 是函数,多数故事函数长一个样:"取出 default export 里的 component,把 args 展开传给组件"。CSF 3 将这种重复工作内置为每个渲染器自带的默认 render。正如文档所说,如果你做的事仅仅是"把 args 展开进组件"(最普遍的场景),就完全不需要写 render——csf-3-example-default-render.md 里等价于"按默认方式渲染"的写法就是一行 export const Basic = {};。
因此极简按钮故事中,只要 meta.component 指向了 Button,Storybook 就会用各渲染器的默认渲染逻辑去实例化组件并把 args 作为属性/插槽/事件传入,无需手工铺陈。
2. Args 是"动态数据":Actions 自动探测回调,Controls 实时可编辑
CSF 文档明确定义:Args 是由 Storybook 及其插件提供(且可能被更新)的动态数据。在 docs/writing-stories/index.mdx 中进一步说明:
Addons can enhance args. For instance, Actions auto-detects which args are callbacks and appends a logging function to them. That way, interactions (like clicks) get logged in the actions panel.
即 Actions 插件会自动识别哪些 args 是回调函数,并为其追加日志能力,于是你在渲染出的按钮上点击时,事件会自动出现在 Actions 面板——无需再手写 action('clicked')。与此同时,Controls 面板基于 Args 提供实时编辑,团队可以动态改动这些值来压测组件边界。这正是 Args 写法相对"render 里写死一切"的写法更具可移植性的原因:它只依赖"组件 + 输入数据"这一抽象,而不再依赖 Actions 的具体调用。
3. 一些渲染器需要"点破"回调 arg
默认渲染与自动探测覆盖了大多数场景,但对事件需要显式声明的渲染器(Vue、Web Components),极简写法在 meta(default export)中通过 argTypes 把 onClick 声明为回调 arg:
- Vue(含 Vue 3)版本声明空的
onClick: {},指示 Controls/Actions 将其作为函数参数处理; - Web Components 版本声明
onClick: { action: 'onClick' },等于在 argTypes 层面显式启用 action 记录,弥补组件基于原生事件(@click)而无法被默认自动识别的缺口。
这正是"极简"与"完整功能"之间的平衡点:组件本身的结构决定了你需要在 meta 中补多少声明。
三、全框架极简写法对照:Angular / React / Solid / Svelte
下面把 button-story-click-handler-simplificated.md 中所有渲染器示例完整展开。每一段都代表"一个会记录点击事件的 Button 故事"在该框架下的最简写法。
Angular
CSF 3
// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';
import { Button } from './button.component';
const meta: Meta<Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<Button>;
export const Text: Story = {
args: {},
};
CSF Next(实验性,🧪)
// Button.stories.ts
import preview from '../.storybook/preview';
import { Button } from './button.component';
const meta = preview.meta({
component: Button,
});
export const Text = meta.story({
args: {},
});
Angular 渲染器的默认渲染会将 args 经 argsToTemplate 一类的映射绑定到组件输入与事件输出(参考带 render 版本中对 argsToTemplate 的注释说明),@Output() onClick 会收到自动附加的 action 日志函数,因此故事正文只需空的 args: {}。
React
CSF 3(JavaScript)
// Button.stories.js|jsx
import { Button } from './Button';
export default {
component: Button,
};
export const Text = {
args: {},
};
CSF 3(TypeScript)
// Button.stories.ts|tsx
// 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 { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Text: Story = {
args: {},
};
注意 TS 写法中 satisfies Meta<typeof Button>:它让 meta 获得严格的组件类型推导,从而后续 StoryObj<typeof meta> 能校验 story 的 args 是否匹配组件 props。文件首行注释也提示了必须把 @storybook/your-framework 替换为你实际使用的框架包(如 react-vite、nextjs、nextjs-vite)。
CSF Next 🧪(TSX / JSX)
// Button.stories.ts|tsx
import preview from '../.storybook/preview';
import { Button } from './Button';
const meta = preview.meta({
component: Button,
});
export const Text = meta.story({
args: {},
});
// Button.stories.js|jsx
import preview from '../.storybook/preview';
import { Button } from './Button';
const meta = preview.meta({
component: Button,
});
export const Text = meta.story({
args: {},
});
Solid
// Button.stories.js|jsx
import { Button } from './Button';
export default {
component: Button,
};
export const Text = {
args: {},
};
// Button.stories.ts|tsx
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
import { Button } from './Button';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Text: Story = {
args: {},
};
Svelte
// Button.stories.js
import Button from './Button.svelte';
export default {
component: Button,
};
export const Text = {
args: {},
};
// Button.stories.ts
// Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
import Button from './Button.svelte';
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Text: Story = {
args: {},
};
Svelte 组件以 props + 事件派发(createEventDispatcher)的形式对外暴露(参见 button-implementation.md 中 Button.svelte 的 export let onClick),默认渲染器会自动完成 props 绑定与事件接线。
四、需要 argTypes 辅助的渲染器:Vue 与 Web Components
Vue(Vue 3,CSF 3 与 CSF Next)
CSF 3
// Button.stories.js
import Button from './Button.vue';
export default {
component: Button,
argTypes: {
onClick: {},
},
};
export const Text = {
args: {},
};
// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import Button from './Button.vue';
const meta = {
title: 'Button',
component: Button,
argTypes: {
onClick: {},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Text: Story = {
args: {},
};
CSF Next 🧪
// Button.stories.ts
import preview from '../.storybook/preview';
import Button from './Button.vue';
const meta = preview.meta({
component: Button,
argTypes: {
onClick: {},
},
});
export const Text = meta.story({
args: {},
});
// Button.stories.js
import preview from '../.storybook/preview';
import Button from './Button.vue';
const meta = preview.meta({
component: Button,
argTypes: {
onClick: {},
},
});
export const Text = meta.story({
args: {},
});
Vue 组件通过 emits: ['click'] 对外发事件、按钮的 props 来自父级模板绑定(见 button-implementation.md 的 Button.vue)。在 meta 中声明空的 argTypes.onClick: {},等于告诉 Storybook"存在一个名为 onClick 的输入";控件面板会为它生成控件,Actions 也能据此把 click 事件记录成 action。若完全省略该声明,默认渲染器可能无法把点击正确连接到日志。CSF Next 版本里这段声明同样出现在 preview.meta({...}) 中。
Web Components
CSF 3
// Button.stories.js
export default {
component: 'custom-button',
argTypes: {
onClick: { action: 'onClick' },
},
};
export const Text = {
args: {},
};
// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/web-components-vite';
const meta: Meta = {
component: 'custom-button',
argTypes: {
onClick: { action: 'onClick' },
},
};
export default meta;
type Story = StoryObj;
export const Text: Story = {
args: {},
};
CSF Next 🧪
// Button.stories.js
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'custom-button',
argTypes: {
onClick: { action: 'onClick' },
},
});
export const Text = meta.story({
args: {},
});
// Button.stories.ts
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'custom-button',
argTypes: {
onClick: { action: 'onClick' },
},
});
export const Text = meta.story({
args: {},
});
Web Components 以自定义元素 + 原生事件为边界(component: 'custom-button',点击对应 @click),事件名与 props 名之间不存在像 React/Vue 那样的"props 即事件"约定。因此这里的 argTypes.onClick 直接写成 { action: 'onClick' }:它显式把该 arg 声明为一个名为 onClick 的 action,从而在默认渲染下把原生 click 上报到 Actions 面板——这正是"极简但仍需向 Storybook 交代组件事件模型"的典型案例。
五、CSF 3 与 CSF Next 的写法差异总览
从上述示例可以清楚归纳出同一故事的两种语法形态:
| 维度 | CSF 3 | CSF Next(实验性 🧪) |
|---|---|---|
| 元数据入口 | 文件默认导出 export default meta |
import preview from '../.storybook/preview' 后调用 preview.meta({...}) |
| 故事声明 | 具名导出对象 export const Text: Story = { args: {} } |
export const Text = meta.story({ args: {} }) |
| 类型助手 | Meta<T> / StoryObj<T>(常配合 satisfies) |
由 preview.meta() / preview 推断,通常无需额外类型标注 |
| 元数据内容 | component、title、argTypes 全部相同 |
完全一致,仅载体不同 |
代码片段中用 tabTitle="CSF Next 🧪" 标注了实验性语法,并在文件名中区分 js|jsx|ts|tsx,便于文档站点按渲染器与语法标签分别渲染。从源码结构可以推断,CSF Next 依赖 .storybook/preview 提供的 preview.meta 与 preview.story 工厂方法(仓库中 code/core 等目录承载相应实现),目标是把类型推导集中到一处、减少样板;但它与 CSF 3 描述的是同一套 Args 数据模型。
六、注意事项与边界
- 替换 your-framework 占位符:TS 示例中的
@storybook/your-framework必须换成实际框架包(如react-vite、nextjs、vue3-vite、sveltekit、svelte-vite、storybook-solidjs-vite等)。 - 默认导出
component字段是必需的:CSF 文档指出component被 addons 用于自动生成属性表(prop table)与展示组件元数据;极简写法全部依赖这一点。 - title 是可选的:CSF 3 支持按文件路径自动推断故事层级标题,不写
title也能工作(参考 docs/api/csf/index.mdx 自动生成标题一节)。 - 回调类 args 的最佳实践:建议让故事名以大写字母开头(
Text),并善用 Controls 面板实时修改 args 来压测组件、寻找边界情况;详见 writing-stories/index.mdx。 - 组件自身的 props 设计:极简写法的成立前提是组件对外暴露的 props/事件足够规整(参考各框架 Button 的 button-implementation.md)。当渲染逻辑超出"把 args 展开进组件"(如需要组合多个组件、依赖 Hooks/Signals、页面级布局)时,仍应回到显式
render的自定义渲染函数形态,例如 button-story.md 中ButtonWithHooks的写法。
七、延伸阅读
- Component Story Format (CSF) 文档:Args story inputs、默认渲染函数、CSF2→CSF3 迁移与 codemod
- 如何编写 stories:Args 复用、组合式故事、Controls 实时编辑、Actions 自动探测
- Actions:回调 args 如何被自动记录与过滤
- Controls:基于 Args 的动态控件编辑
- 同一故事的三版对照: button-story-click-handler.md → button-story-click-handler-args.md → button-story-click-handler-simplificated.md
掌握"纯 Args + 默认渲染"的极简范式,意味着你能用最少的代码描述大多数组件故事,同时让故事天然具备 Controls 可编辑、Actions 可记录、跨 Storybook 生态可移植的特性——这正是 CSF 面向 Args 与插件的设计目标所在。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00