Storybook args 实战:不改组件源码给组件传参的完整指南
2026/9/18 13:28:34 网站建设 项目流程

Storybook args 实战:不改组件源码给组件传参的完整指南

Storybook args 是用一个普通的 JavaScript 对象动态驱动组件 props、插槽、样式与输入的统一机制,全程无需修改组件源码。下面按实战路径走一遍给组件传参写故事的完整流程:CSF 3 写下第一个故事、一张表对照八大框架差异、搞清三层作用域的合并优先级,再掌握 URL 覆盖、Controls 实时编辑与 useArgs 这些进阶技巧。

同一个模板你写了三遍,参数还得重新填一遍

假设你给 Button 组件写了 primary、secondary、disabled 三个故事,用纯模板写法意味着每个故事里都要手工重复一遍全部 props,按钮文案一改就得改三处。args 就是为这个问题设计的。

它的本质是一个普通 JS 对象:故事里写args: { primary: true, label: 'Button' },Storybook 会把这组值翻译成各框架对应的 props、@Input()或插槽输入,让组件按这组值渲染。更关键的一点:任一 arg 的值变化,组件就会重新渲染——这正是 Controls 等 addon 能在面板里"实时编辑组件"的底层原因。

截图左侧选中 Button 的 Primary 故事,预览区按 args 渲染出 primary 态按钮,底部 Controls 面板里 primary 开关、label 文本等参数一改就立即重新渲染。whats-a-story.mdx 对同一示例有完整的交互演示。

⏱️ 30 秒跑通第一个 args 故事

先以 React + TypeScript(CSF 3,当前主流写法)完整走一遍,后面看其他框架时只关心差异部分。以下是与组件源码同目录的Button.stories.ts,仅用于开发期、不会进生产构建:

import type { Meta, StoryObj } from '@storybook/react-vite'; // 换成你用的框架包,如 nextjs、nextjs-vite import { Button } from './Button'; // meta:描述"组件本身",通过默认导出交给 Storybook const meta = { component: Button, } satisfies Meta<typeof Button>; // 校验字段写对,同时保留 Button 的字面类型 export default meta; type Story = StoryObj<typeof meta>; // 从 meta 反推 args/render 的类型,获得自动补全 export const Primary: Story = { // args:描述"某一个状态",只对这个故事生效 args: { primary: true, label: 'Button', }, };

故事文件的分工是两条线:meta(默认导出)描述组件本身——渲染哪个组件、侧边栏怎么组织、addon 怎么消费它;具名导出各是一个独立故事,其中args是 JSON 可序列化的对象(字符串键 + 合法值),描述"这个状态需要哪些参数、取值是什么"。

类型桥接为什么这么写?satisfies Meta<typeof Button>负责"校验不扩散":字段拼错直接标红,但不会把 meta 放宽成宽泛的Meta类型;接着StoryObj<typeof meta>拿 Button 的真实 props 给argsrender做类型校验和补全——label 填成数字会立刻报错。写 JS 的话删掉两行类型桥接即可,运行时行为一致。

其他框架长什么样:一张表看懂差异

args 的结构在所有框架里始终一致,变的只有"component 指向什么"和"要不要自己写 render":

框架component 指向需要 render类型导入包
React组件引用不需要(自动渲染)@storybook/react-vite(或 nextjs 等)
Vue 3.vue组件需要(用 v-bind 把 args 传给组件)@storybook/vue3-vite
Angular组件类不需要(直接绑定@Input@storybook/angular
Svelte.svelte组件不需要@storybook/svelte-vite/ sveltekit
Preact组件引用需要(JSX 里展开)@storybook/preact-vite
Solid组件引用不需要storybook-solidjs-vite
HTML无框架运行时需要(手写 DOM 节点)@storybook/html
Web Components自定义元素名字符串不需要@storybook/web-components-vite

挑两段关键差异看。Vue 3 的故事文件(Button.stories.ts,JS 版去掉类型导入即可),render 里把 args 以v-bind一次性透传:

export const Primary: Story = { render: (args) => ({ components: { Button }, setup() { return { args }; }, template: '<Button v-bind="args" />', }), args: { primary: true, label: 'Button', }, };

HTML 渲染器没有框架运行时,Button.stories.js里要手动把 args 组装成 DOM:

export const Primary = { render: (args) => { const btn = document.createElement('button'); btn.innerText = args.label; // 消费 args 的值 const mode = args.primary ? 'storybook-button--primary' : 'storybook-button--secondary'; btn.className = ['storybook-button', 'storybook-button--medium', mode].join(' '); return btn; }, args: { primary: true, label: 'Button', }, };

只要在 render 里消费 args,Controls、URL 参数等一切依赖 args 的能力照常工作。Web Components 则是唯一component不指向模块的框架——它取自定义元素名(如component: 'demo-button');元素名没法参与类型推导,TS 版退化为宽泛的type Story = StoryObj,args 的约束交给组件自身的 attribute/property 定义。

Svelte 的额外选项:Svelte CSF

社区维护的@storybook/addon-svelte-csf提供了更贴近模板直觉的写法:defineMeta描述组件,Story组件以 props 形式接收nameargs,和标准 CSF 3 二选一:

<script module> import { defineMeta } from '@storybook/addon-svelte-csf'; import Button from './Button.svelte'; const { Story } = defineMeta({ component: Button, }); </script> <Story name="Primary" args={{ primary: true, label: 'Button' }} />

一个限制要记住:Svelte CSF 下不能用args传插槽内容(children),内容要写在<Story>开闭标签之间、作为childrensnippet prop 传入;若改用渲染完全由 children 决定的asChild形式,Controls 这类依赖 args 的能力就不可用了。

🧩 CSF Next:preview.meta() 改掉了哪三点

带 🧪 实验标记的 CSF Next 把"默认导出 + 具名导出"的隐式约定改写成了链式 API,想提前尝鲜新 API 就看这里:

import preview from '../.storybook/preview'; // meta 来自 preview 模块,不再是本地对象 import { Button } from './Button'; const meta = preview.meta({ component: Button, // 类型由组件自动反推 }); export const Primary = meta.story({ args: { primary: true, label: 'Button', }, });

与 CSF 3 的差异就三点:

  1. meta 不再本地书写,而是从.storybook/preview导入preview后,用preview.meta()显式创建;
  2. 故事不再是"具名导出的普通对象",而是经meta.story()链式创建;
  3. 复用取参的路径变了:原来...Primary.args要写成...Primary.input.args,因为 CSF Next 里故事是带input字段的函数对象。

类型方面,preview.meta()会从组件直接反推出 meta 的具体类型,satisfies/StoryObj这层手写桥接不再需要。

args 从哪来:三层作用域与优先级

同一个键出现在多处时,到底谁说了算?args 可以在三个层级定义,三层都是普通 JS 对象:

层级定义位置作用范围
Global argspreview.*的默认导出每个组件的所有故事
Component argsCSF 默认导出的args键(Svelte CSF 里是defineMeta的属性)当前组件的所有故事
Story args故事对象的args仅当前故事

优先级一条线:global → component → story,后写的覆盖先写的,故事级最高

源码 prepareStory.ts 里能直接看到证据,故事准备阶段按这个顺序展开合并:

const passedArgs: Args = { ...projectAnnotations.args, // global ...componentAnnotations.args, // component ...storyAnnotations?.args, // story(优先级最高) } as Args;

合并后的initialArgs还会流经 argsEnhancers 流水线(例如从 argTypes 推导默认值),整个加工都发生在故事"准备"阶段,与组件自身的 props 声明完全解耦——这也印证了 args 不改组件源码的设定。

两条实操建议:大多数故事共享的 args 应上提到 component args;"全局统一设置"(比如主题切换)场景更适合用 globals 而不是 global args,因为 globals 能挂在工具栏菜单里让用户直接切换取值。

复用与组合:args 别复制着写

写完第一个故事后马上会遇到重复问题,有三招由轻到重。

对象展开:args 就是普通对象,ES2015 展开即可复用,这是最轻的一招:

export const Secondary: Story = { args: { ...Primary.args, // 继承 Primary 的全部参数 primary: false, // 只覆盖一个 }, };

上提到 component args:当同一组件的大部分故事都在复用同一组 args,就别在每个故事里展开,直接写进 meta 的默认导出——比如把primary: true放进 component args,所有 Button 故事默认变 primary,单个故事仍可覆盖。

复合组件:当组件由多个子组件拼装而成、故事参数原样透传给子组件时,可以导入子组件的故事、直接组合它们的 args。例如 Page 的"已登录"故事直接复用 Header 对应故事的参数:

// 导入 Header 的全部故事 import * as HeaderStories from './Header.stories'; export const LoggedIn: Story = { args: { ...HeaderStories.LoggedIn.args, // 组合参数 = 直接拼装子故事的 args }, };

🔗 从面板到 URL:覆盖、mapping 与 useArgs

除了面板,args 还有三个"不打开故事文件也能操作"的入口:写进 URL、Controls 实时编辑、从组件内部驱动。

URL 覆盖怎么编码

URL 里的args恒为一组key: value对,用分号分隔,典型 Controls 链接:

?path=/story/avatar--default&args=style:rounded;size:100

特殊值按下表编码:

场景编码规则示例
对象、数组直接嵌套args=obj.key:val;arr[0]:one;arr[1]:two
null / undefined!前缀args=nil:!null
日期!date(value),值为 ISO 日期串args=birthday:!date(1990-01-01)
颜色!hex/!rgba/!hsla,rgb(a)/hsl(a) 不能含空格与百分号args=color:!hex(f0f)

出于 XSS 防护,URL args 的键值只允许字母数字、空格、下划线与连字符,其余类型会被忽略并从 URL 移除——但仍可通过 Controls 面板或故事内部使用。URL 中写出的 args 会扩展并覆盖故事上默认的 args。

JSX 这类没法序列化的值怎么办

JSX 元素这类复杂值无法序列化到 manager(Controls 面板)或同步到 URL,解法是argTypes里的mapping:用简单字符串"映射"到复杂类型,搭配select控件最合理:

const meta = { component: Example, argTypes: { label: { control: { type: 'select' }, options: ['Normal', 'Bold', 'Italic'], mapping: { Bold: <b>Bold</b>, // 键对应 arg 的值,不是 options 的下标 Italic: <i>Italic</i>, }, }, }, } satisfies Meta<typeof Example>;

mapping不必穷尽:当前值不在 mapping 键里时直接使用原值。

useArgs:把组件交互反写进面板

args 写进故事后,两个面板自动到位:组件的回调会记录进Actions面板(点一下就能看到事件参数);组件的参数会出现在Controls面板,可实时编辑并即时触发重渲染。反过来——想让组件内部交互驱动 args(比如复选框被点击后,Controls 里的开关状态跟着翻转)——在 render 里用storybook/preview-api导出的useArgs

import { useArgs } from 'storybook/preview-api'; export const Example: Story = { args: { isChecked: false, label: 'Try Me!', }, render: function Render(args) { // 取当前 args 值与更新函数 const [{ isChecked }, updateArgs] = useArgs(); function onChange() { updateArgs({ isChecked: !isChecked }); // 把交互结果反写回 args } return <Checkbox {...args} onChange={onChange} isChecked={isChecked} />; }, };

⚠️ 官方明确警告:在 render 函数里用了 Storybook 的 hooks API,就不要再混用 React 的useState/useEffect/useRef——React hooks 引发的副作用与重渲染不经过 Storybook 的 hook 上下文,二次渲染会直接报错。状态与副作用请统一改用storybook/preview-api提供的同名等价 hooks。

接下来读什么

仓库里有三个入口,按"概念 → 详解 → 源码"的顺序:

  • whats-a-story.mdx:认识"故事是什么"的第一篇,同一 Button 示例的入门演示;
  • args.mdx:三层作用域、组合、URL 覆盖、mapping 与 useArgs 的权威出处,写作规范细节可在 index.mdx 中补齐;
  • prepareStory.ts:prepareStory把"故事 + 全部装饰器 + 参数"打包成可重复调用的无状态渲染函数,想深挖合并逻辑从这一处入手。

最后把贯穿全程的心智模型收成一句:写故事 = 一组 args + 一个渲染目标。三层作用域、面板、URL,都不过是这组 args 的不同书写入口。

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

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

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

立即咨询