Storybook args 实战:不改组件源码给组件传参的完整指南
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 TypeScriptCSF 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 Metatypeof Button; // 校验字段写对同时保留 Button 的字面类型 export default meta; type Story StoryObjtypeof meta; // 从 meta 反推 args/render 的类型获得自动补全 export const Primary: Story { // args描述某一个状态只对这个故事生效 args: { primary: true, label: Button, }, };故事文件的分工是两条线meta默认导出描述组件本身——渲染哪个组件、侧边栏怎么组织、addon 怎么消费它具名导出各是一个独立故事其中args是 JSON 可序列化的对象字符串键 合法值描述这个状态需要哪些参数、取值是什么。类型桥接为什么这么写satisfies Metatypeof Button负责校验不扩散字段拼错直接标红但不会把 meta 放宽成宽泛的Meta类型接着StoryObjtypeof meta拿 Button 的真实 props 给args、render做类型校验和补全——label 填成数字会立刻报错。写 JS 的话删掉两行类型桥接即可运行时行为一致。其他框架长什么样一张表看懂差异args 的结构在所有框架里始终一致变的只有component 指向什么和要不要自己写 render框架component 指向需要 render类型导入包React组件引用不需要自动渲染storybook/react-vite或 nextjs 等Vue 3.vue组件需要用 v-bind 把 args 传给组件storybook/vue3-viteAngular组件类不需要直接绑定Inputstorybook/angularSvelte.svelte组件不需要storybook/svelte-vite/ sveltekitPreact组件引用需要JSX 里展开storybook/preact-viteSolid组件引用不需要storybook-solidjs-viteHTML无框架运行时需要手写 DOM 节点storybook/htmlWeb Components自定义元素名字符串不需要storybook/web-components-vite挑两段关键差异看。Vue 3 的故事文件Button.stories.tsJS 版去掉类型导入即可render 里把 args 以v-bind一次性透传export const Primary: Story { render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { primary: true, label: Button, }, };HTML 渲染器没有框架运行时Button.stories.js里要手动把 args 组装成 DOMexport 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 里消费 argsControls、URL 参数等一切依赖 args 的能力照常工作。Web Components 则是唯一component不指向模块的框架——它取自定义元素名如component: demo-button元素名没法参与类型推导TS 版退化为宽泛的type Story StoryObjargs 的约束交给组件自身的 attribute/property 定义。Svelte 的额外选项Svelte CSF社区维护的storybook/addon-svelte-csf提供了更贴近模板直觉的写法defineMeta描述组件Story组件以 props 形式接收name与args和标准 CSF 3 二选一script module import { defineMeta } from storybook/addon-svelte-csf; import Button from ./Button.svelte; const { Story } defineMeta({ component: Button, }); /script Story namePrimary args{{ primary: true, label: Button }} /一个限制要记住Svelte CSF 下不能用args传插槽内容children内容要写在Story开闭标签之间、作为childrensnippet prop 传入若改用渲染完全由 children 决定的asChild形式Controls 这类依赖 args 的能力就不可用了。 CSF Nextpreview.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 的差异就三点meta 不再本地书写而是从.storybook/preview导入preview后用preview.meta()显式创建故事不再是具名导出的普通对象而是经meta.story()链式创建复用取参的路径变了原来...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--defaultargsstyle:rounded;size:100特殊值按下表编码场景编码规则示例对象、数组直接嵌套argsobj.key:val;arr[0]:one;arr[1]:twonull / undefined!前缀argsnil:!null日期!date(value)值为 ISO 日期串argsbirthday:!date(1990-01-01)颜色!hex/!rgba/!hslargb(a)/hsl(a) 不能含空格与百分号argscolor:!hex(f0f)出于 XSS 防护URL args 的键值只允许字母数字、空格、下划线与连字符其余类型会被忽略并从 URL 移除——但仍可通过 Controls 面板或故事内部使用。URL 中写出的 args 会扩展并覆盖故事上默认的 args。JSX 这类没法序列化的值怎么办JSX 元素这类复杂值无法序列化到 managerControls 面板或同步到 URL解法是argTypes里的mapping用简单字符串映射到复杂类型搭配select控件最合理const meta { component: Example, argTypes: { label: { control: { type: select }, options: [Normal, Bold, Italic], mapping: { Bold: bBold/b, // 键对应 arg 的值不是 options 的下标 Italic: iItalic/i, }, }, }, } satisfies Metatypeof Example;mapping不必穷尽当前值不在 mapping 键里时直接使用原值。useArgs把组件交互反写进面板args 写进故事后两个面板自动到位组件的回调会记录进Actions面板点一下就能看到事件参数组件的参数会出现在Controls面板可实时编辑并即时触发重渲染。反过来——想让组件内部交互驱动 args比如复选框被点击后Controls 里的开关状态跟着翻转——在 render 里用storybook/preview-api导出的useArgsimport { 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.tsprepareStory把故事 全部装饰器 参数打包成可重复调用的无状态渲染函数想深挖合并逻辑从这一处入手。最后把贯穿全程的心智模型收成一句写故事 一组 args 一个渲染目标。三层作用域、面板、URL都不过是这组 args 的不同书写入口。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考