首页
/ Storybook 组件故事的极简范式:用 Args 编排点击事件的全框架写法(CSF 3 与 CSF Next)

Storybook 组件故事的极简范式:用 Args 编排点击事件的全框架写法(CSF 3 与 CSF Next)

2026-09-07 11:59:56作者:仰钰奇

在 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" 小节。官方文档在引出它之前,先展示了同一个按钮故事的两种更"啰嗦"的写法:

  1. button-story-click-handler.md:不使用 Args,用 render 硬编码组件属性,并在渲染函数内部直接调用 action('clicked') 生成回调,例如 React 的 render: () => <Button label="Hello" onClick={action('clicked')} />
  2. button-story-click-handler-args.md:开始引入 Args,把 labelonClick 挪进 args,再用 render: ({ label, onClick }) => <Button .../> 消费它们;
  3. 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)中通过 argTypesonClick 声明为回调 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 渲染器的默认渲染会将 argsargsToTemplate 一类的映射绑定到组件输入与事件输出(参考带 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-vitenextjsnextjs-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 推断,通常无需额外类型标注
元数据内容 componenttitleargTypes 全部相同 完全一致,仅载体不同

代码片段中用 tabTitle="CSF Next 🧪" 标注了实验性语法,并在文件名中区分 js|jsx|ts|tsx,便于文档站点按渲染器与语法标签分别渲染。从源码结构可以推断,CSF Next 依赖 .storybook/preview 提供的 preview.metapreview.story 工厂方法(仓库中 code/core 等目录承载相应实现),目标是把类型推导集中到一处、减少样板;但它与 CSF 3 描述的是同一套 Args 数据模型。

六、注意事项与边界

  1. 替换 your-framework 占位符:TS 示例中的 @storybook/your-framework 必须换成实际框架包(如 react-vitenextjsvue3-vitesveltekitsvelte-vitestorybook-solidjs-vite 等)。
  2. 默认导出 component 字段是必需的:CSF 文档指出 component 被 addons 用于自动生成属性表(prop table)与展示组件元数据;极简写法全部依赖这一点。
  3. title 是可选的:CSF 3 支持按文件路径自动推断故事层级标题,不写 title 也能工作(参考 docs/api/csf/index.mdx 自动生成标题一节)。
  4. 回调类 args 的最佳实践:建议让故事名以大写字母开头(Text),并善用 Controls 面板实时修改 args 来压测组件、寻找边界情况;详见 writing-stories/index.mdx
  5. 组件自身的 props 设计:极简写法的成立前提是组件对外暴露的 props/事件足够规整(参考各框架 Button 的 button-implementation.md)。当渲染逻辑超出"把 args 展开进组件"(如需要组合多个组件、依赖 Hooks/Signals、页面级布局)时,仍应回到显式 render 的自定义渲染函数形态,例如 button-story.mdButtonWithHooks 的写法。

七、延伸阅读

掌握"纯 Args + 默认渲染"的极简范式,意味着你能用最少的代码描述大多数组件故事,同时让故事天然具备 Controls 可编辑、Actions 可记录、跨 Storybook 生态可移植的特性——这正是 CSF 面向 Args 与插件的设计目标所在。

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