用 Storybook Args 一个对象切换 Button 全部状态:React / Vue / Angular 跨框架写法与三层优先级全解
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
你手里有个Button,要演示出紫色主按钮、灰色次按钮、加载中、禁用这几种样子。最笨的办法是往源码里塞四个if分支?不必。Storybook 的Args机制让你用一个普通对象{ primary: true, label: 'Button' }就能把同一份组件切成不同状态,一行组件源码都不用碰。下面把"怎么用"和"为什么能用"一次讲清。
一个 args 对象能换掉多少种按钮状态
一句话结论:args 就是一张"渲染参数表",键是 prop 名,值是这个状态该长什么样。故事文件里export出来的每个对象就是一个状态,它带的args决定组件怎么被喂料。
最小的可运行故事,去掉所有类型糖之后只剩两行:
export const Primary = { args: { primary: true, label: 'Button' }, };改primary为false、label为别的字符串,按钮的视觉状态立刻跟着变——因为 Storybook 把这张表原样送进了组件。记住这个形状,后面所有框架只是在"怎么把表送进组件"上做文章。
React 版 Button 故事怎么写
React 里 args 就是 props 本身,不需要额外渲染函数。下面是一份完整可跑的 TS 故事文件(JS 项目删掉satisfies和type那两行即可):
// 只保留 TS 写法;把 react-vite 换成你实际用的框架包 import type { Meta, StoryObj } from '@storybook/react-vite'; import { Button } from './Button'; const meta = { component: Button, } satisfies Meta<typeof Button>; // 让 meta 的字段被 Button 的真实 props 约束 export default meta; type Story = StoryObj<typeof meta>; // 从此 args 有自动补全和类型检查 export const Primary: Story = { args: { primary: true, label: 'Button', }, };差异点:React 靠typeof Button把类型"反推"出来,所以连render都不用写,args 直接当 props 消费。这是三个框架里最省事的一个。
Vue 3 版 Button 故事怎么写
Vue 组件是.vue单文件,Storybook 猜不出怎么把普通对象绑上去,所以必须手写一个render,用v-bind当那根连接线:
import type { Meta, StoryObj } from '@storybook/vue3-vite'; import Button from './Button.vue'; const meta = { component: Button, } satisfies Meta<typeof Button>; export default meta; type Story = StoryObj<typeof meta>; export const Primary: Story = { // 把整张 args 表透传给组件,等价于模板里的 :props render: (args) => ({ components: { Button }, setup() { return { args }; }, template: '<Button v-bind="args" />', }), args: { primary: true, label: 'Button', }, };差异点:相比 React,Vue 多了render,但 args 的形状一模一样;v-bind="args"就是"把表整个贴到组件上"的 Vue 语法。
Angular 版 Button 故事怎么写
Angular 连render都省了,args会被直接绑到组件的@Input上。类型桥接走的是另一条路——把组件类本身丢给Meta:
import type { Meta, StoryObj } from '@storybook/angular'; import { Button } from './button.component'; const meta: Meta<Button> = { component: Button, // 类型参数直接吃组件类,args 与 @Input 自动对齐 }; export default meta; type Story = StoryObj<Button>; export const Primary: Story = { args: { primary: true, label: 'Button', }, };差异点:React 是"先typeof Button再反推",Angular 是"直接把类当类型参数"。两种绕法,目的相同:让 args 跟组件输入保持类型同步,写错键名时编辑器就红给你看。
三种框架的 component、render、类型桥接对比
| 框架 | component指向 | render必需? | 类型桥接方式 |
|---|---|---|---|
| React | 组件模块Button | 否,args 即 props | satisfies Meta<typeof Button>+StoryObj<typeof meta> |
| Vue 3 | .vue单文件组件 | 是,v-bind透传 | 同 React |
| Angular | 组件类Button | 否,自动绑@Input | Meta<Button>直接传类 |
一张表看下来,真正的区别只有两处:要不要手写 render,以及类型从哪来。args 本身跨框架通用,这正是它能"一改全改"的根基。
三层 args 谁覆盖谁:global、component、story 优先级
当同一个参数可能在三个地方被定义时,谁说了算?规则是越具体越优先:
| 层级 | 写在哪 | 作用范围 | 优先级 |
|---|---|---|---|
| story args | 单个故事对象 | 只影响该故事 | 最高 |
| component args | meta的args键 | 该组件所有故事 | 中 |
| global args | .storybook/preview默认导出 | 所有组件所有故事 | 最低 |
这不是口头约定,源码里写得很直白。在 code/core/src/preview-api/modules/store/csf/prepareStory.ts 中,故事"准备"阶段按固定顺序展开三层:
const passedArgs: Args = { ...projectAnnotations.args, // 全局 ...componentAnnotations.args, // 组件 ...storyAnnotations?.args, // 故事,写在最后,覆盖前两者 };对象展开"后者吃前者",所以故事级永远赢。这段逻辑发生在渲染之前、和组件 props 声明完全解耦,也解释了为什么"用 args 切状态"永远不需要改组件源码。另外注意:合并完的表还会再过一遍argsEnhancers流水线,比如从argTypes推导默认值,就是在这一步注入的。
提醒一句:如果你要的是"用户在工具栏一键切换"的全局开关(典型如主题),用globals比 global args 更合适——前者能被工具栏控件直接驱动,后者是写死的默认值。
实战技巧:args 复用、URL 覆写、Controls 实时联动
① 对象展开复用。args 就是个普通对象,想派生一个"长得差不多"的新状态,抄一份再改单个键即可:
export const PrimaryLongName: Story = { args: { ...Primary.args, // 先搬走 Primary 的全部参数 label: '很长的按钮文本', // 只覆盖一个键 }, };如果绝大多数故事共享一组值,别在故事里各抄一遍,把它上提到meta的args(component args)更干净。
② URL 直接覆写参数。不想开页面调 Controls?把参数拼进 query 就能复现现场,方便贴给同事:
?path=/story/button--primary&args=label:你好;size:large解析规则:值以;分隔,写成key: value;会按argTypes自动转回对应类型,对象和数组都支持。特殊值要加!前缀,比如nil:!null表示空值,颜色写!hex(...)/!rgba(...)。出于 XSS 防护,URL 里的值只认字母数字、空格、下划线和连字符,其余会被静默忽略——复杂值请走 Controls 面板或argTypes.mapping。
③ Controls 与 useArgs 双向联动。只要故事用了 args,Controls 面板就能实时改值并触发重渲染,回调事件会记进 Actions 面板。反过来,当你要"组件内部状态改 → Controls 选中态跟着变",就在渲染函数里用useArgs:
render: (args) => { const [{ isChecked }, updateArgs] = useArgs(); return ( <Checkbox {...args} isChecked={isChecked} onChange={() => updateArgs({ isChecked: !isChecked })} // 点组件反写 args /> ); }这里有个坑必须说:在渲染函数里用 Storybook 的 hooks 时,别再混 React 自己的useState/useEffect/useRef。React 那套副作用不走 Storybook 的 hook 上下文,二次渲染就会炸;状态统一交给storybook/preview-api的等价 hooks。
心智模型与延伸阅读
把整件事压缩成一句话:写故事 = 描述一组 args + 一个渲染目标。args 是"输入契约",render(或框架的自动绑定)是"执行器",Storybook 在 prepare 阶段把三层 args 合并、增强后喂给执行器,于是同一个组件才能被同一份源码反复切成 N 种状态。你真正要做的,只是把"想要什么状态"写成一张表。
想继续往下挖,三个入口:
- 参数机制权威出处:docs/writing-stories/args.mdx,三层作用域、组合、URL 覆写、
mapping、useArgs全在这。 - "故事到底是什么"的第一课:docs/get-started/whats-a-story.mdx,配了运行截图。
- 合并顺序的底层实现:code/core/src/preview-api/modules/store/csf/prepareStory.ts,看
prepareStory如何把"故事 + 装饰器 + 参数"打包成一个可复用的无状态渲染函数。
【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考