ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在 Storybook 中巧用 args 组合故事:以 Primary / Secondary / Tertiary 多状态编写为例

在 Storybook 中巧用 args 组合故事:以 Primary / Secondary / Tertiary 多状态编写为例 在 Storybook 中巧用 args 组合故事以 Primary / Secondary / Tertiary 多状态编写为例【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook本篇技术指南以 Storybook 官方文档片段 button-story-using-args.md 为骨架完整讲解如何利用args参数描述一个组件的多个渲染状态、通过对象展开复用已有 Story 的数据并给出 Angular、HTML、React、Solid、Svelte、Vue、Web Components 七大渲染器在 CSF 3、CSF Next预览与 Svelte CSF 三种写法下的逐字等价代码。读完你可以在自己项目中直接照抄写出 Primary / Secondary / Tertiary 这类由同一组基础参数派生出的故事并理解它们为何能被 Controls 面板实时编辑。该片段在实际文档中承载于 How to write stories 的 “How to write stories / Using args” 小节见 docs/writing-stories/index.mdx#L263-L305与 Writing stories 的 args 专题、CSF API 参考、CSF Next 参考 相互印证下文将按“概念 → 逐框架代码 → 底层机制 → 进阶组合”的线索展开。一、先理解两个关键概念1.1 什么是 “story” 与 “args”在 Storybook 中一个 story 描述的是某个 UI 组件在某组特定参数下的渲染状态。官方把它概括为A story is an object that describes how to render a component. You can have multiple stories per component, and those stories can build upon one another.而args是 Storybook 用来“描述这些参数”的统一名词——它把 React 的props、Vue 的props、Angular 的Input、Web Components 的 attribute/property、Svelte 的 props 等各框架“向组件传值”的概念抽象成一个可序列化的普通 JavaScript 对象见 docs/writing-stories/args.mdx#L17-L18 对args对象“JSON serializable”的说明。你无需修改组件本身的代码就可以通过 args 动态改变组件的 props、slots、样式、输入等。当一个 arg 的值发生变化时组件会随之重新渲染——这正是后面 Controls 面板能够“现场调参”的基础。1.2 为什么一个组件要写多个故事同一个Button组件在真实界面中会以不同状态出现主按钮、次按钮、带 emoji 的按钮……在 Storybook 中你并不需要为每个状态复制一份代码而是让这些 story “build upon one another”——后面的 story 复用前面 story 的参数只覆盖自己需要改变的那一项。本片段给出的就是最经典的三态写法Primary定义基线参数黄底、文案 “Button”Secondary{ ...Primary.args, label: }继承全部基线参数只把label换成 emojiTertiary同样的展开复用只把label换成另一组 emoji。展开复用带来的维护收益是明确的将来按钮的backgroundColor等公共外观参数需要调整时只需改动Primary一处Secondary与Tertiary会自动同步。下面各框架的代码清单将反复出现同一个模式。二、逐框架完整代码对照说明以下各代码块是原始片段在各渲染器、各语法变体下的完整内容仅移除注释中的外链其余逐字保留。JS 与 TS 变体在正文中完全相同的只呈现一份并注明其同时适用于.js/.jsx与.ts/.tsx。2.1 AngularCSF 3 与 CSF NextAngular 中 args 对应组件Input因此直接书写backgroundColor、label两个输入即可无需render函数import type { Meta, StoryObj } from storybook/angular; import { Button } from ./button.component; const meta: MetaButton { component: Button, }; export default meta; type Story StoryObjButton; export const Primary: Story { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary: Story { args: { ...Primary.args, label: , }, }; export const Tertiary: Story { args: { ...Primary.args, label: , }, };CSF Next预览版本用工厂函数preview.meta(...)声明 meta、用meta.story(...)声明故事复用时访问的是Primary.input.args详见本文第五节对input/composed的说明import preview from ../.storybook/preview; import { Button } from ./button.component; const meta preview.meta({ component: Button, }); export const Primary meta.story({ args: { backgroundColor: #ff0, label: Button, }, }); export const Secondary meta.story({ args: { ...Primary.input.args, label: , }, }); export const Tertiary meta.story({ args: { ...Primary.input.args, label: , }, });2.2 HTMLVanilla 渲染器HTML 渲染器没有“自动把 args 透传给组件”的默认渲染能力因此必须为每个 story 提供render: (args) createButton(args)渲染函数title属性可选省略时 Storybook 会在构建期根据 stories 文件的路径自动推导标题相关机制见 docs/configure/index.mdx 中关于 automatic titles 的介绍import { createButton } from ./Button; export default { /* The title prop is optional. */ title: Button, }; /* * Render functions are a framework specific feature to allow you control on how the component renders. */ export const Primary { render: (args) createButton(args), args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary { render: (args) createButton(args), args: { ...Primary.args, label: , }, }; export const Tertiary { render: (args) createButton(args), args: { ...Primary.args, label: , }, };TypeScript 变体加入MetaButtonArgs/StoryObjButtonArgs类型标注并把createButton与参数类型ButtonArgs一并从./Button导入import type { Meta, StoryObj } from storybook/html; import { createButton, ButtonArgs } from ./Button; const meta: MetaButtonArgs { title: Button, }; export default meta; type Story StoryObjButtonArgs; export const Primary: Story { render: (args) createButton(args), args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary: Story { render: (args) createButton(args), args: { ...Primary.args, label: , }, }; export const Tertiary: Story { render: (args) createButton(args), args: { ...Primary.args, label: , }, };2.3 ReactCSF 3 与 CSF NextReact 的默认渲染即为“用 meta 中的组件 story 的 args 渲染”因此代码最干净只需导出三个命名故事import { Button } from ./Button; export default { component: Button, }; export const Primary { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary { args: { ...Primary.args, label: , }, }; export const Tertiary { args: { ...Primary.args, label: , }, };TypeScript 推荐用satisfies Metatypeof Button约束 meta保证component与 args 类型一致// 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 Metatypeof Button; export default meta; type Story StoryObjtypeof meta; export const Primary: Story { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary: Story { args: { ...Primary.args, label: , }, }; export const Tertiary: Story { args: { ...Primary.args, label: , }, };CSF Next 版本中 JS 与 TS 的文件体完全一致仅在.js|jsx/.ts|tsx扩展名与导入路径上有差异复用基线参数改为Primary.input.argsimport preview from ../.storybook/preview; import { Button } from ./Button; const meta preview.meta({ component: Button, }); export const Primary meta.story({ args: { backgroundColor: #ff0, label: Button, }, }); export const Secondary meta.story({ args: { ...Primary.input.args, label: , }, }); export const Tertiary meta.story({ args: { ...Primary.input.args, label: , }, });2.4 SolidSolid 的非 TS 写法与 React 的 CSF 3 几乎一致import { Button } from ./Button 默认导出 metaimport { Button } from ./Button; export default { component: Button, }; export const Primary { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary { args: { ...Primary.args, label: , }, }; export const Tertiary { args: { ...Primary.args, label: , }, };Solid 的 TS 写法类型定义改从storybook-solidjs-vite导入这是 Solid 官方在社区端发布 SolidJS 集成时使用的包名需在你的项目中已安装对应渲染器为前提import type { Meta, StoryObj } from storybook-solidjs-vite; import { Button } from ./Button; const meta { component: Button, } satisfies Metatypeof Button; export default meta; type Story StoryObjtypeof meta; export const Primary: Story { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary: Story { args: { ...Primary.args, label: , }, }; export const Tertiary: Story { args: { ...Primary.args, label: , }, };2.5 SvelteSvelte CSF 与标准 CSF 两种流派Svelte 有两种声明故事的方式一是社区驱动的Svelte CSFstorybook/addon-svelte-csf使用script module中的defineMeta与模板里的Story组件二是与其他框架一致的标准 CSF 命名导出。官方文档说明Svelte CSF 下 JS 与 TS 文件的代码体完全相同仅为language标注差异因此这里合并为一份script module import { defineMeta } from storybook/addon-svelte-csf; import Button from ./Button.svelte; const { Story } defineMeta({ component: Button, }); /script Story namePrimary args{{ backgroundColor: #ff0, label: Button, }} / Story nameSecondary args{{ backgroundColor: #ff0, label: , }} / Story nameTertiary args{{ backgroundColor:#ff0, label: , }} /注意Svelte CSF 的三个Story各自显式书写了完整的 args原始文档如此。它天然受限于“无法在模板里用对象展开引用上一步的参数”如需真正的程序化复用建议使用标准 CSFimport Button from ./Button.svelte; export default { component: Button, }; export const Primary { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary { args: { ...Primary.args, label: , }, }; export const Tertiary { args: { ...Primary.args, label: , }, };Svelte 的 TS 变体通过import type { Meta, StoryObj } from storybook/your-framework获得类型支持实际使用时把your-framework换成svelte-vite或sveltekit// Replace your-framework with svelte-vite or sveltekit import type { Meta, StoryObj } from storybook/your-framework; import Button from ./Button.svelte; const meta { component: Button, } satisfies Metatypeof Button; export default meta; type Story StoryObjtypeof meta; export const Primary: Story { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary: Story { args: { ...Primary.args, label: , }, }; export const Tertiary: Story { args: { ...Primary.args, label: , }, };2.6 Vue 3Vue 的渲染需要显式render函数返回{ components: { Button }, setup() { return { args } }, template: Button v-bindargs / }本质是把每个 arg 通过v-bindargs展开绑定到组件的 props 上import Button from ./Button.vue; export default { component: Button, }; /* * Render functions are a framework specific feature to allow you control on how the component renders. */ export const Primary { render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary { args: { ...Primary.args, label: , }, render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), }; export const Tertiary { args: { ...Primary.args, label: , }, render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), };Vue 的 TSCSF 3变体引入Meta/StoryObj类型。此处可留意一个细节原始文档中 Vue 示例使用的背景色参数名在 JS 变体里是backgroundColor、在部分 TS 变体里是background两者均为“示意性”命名实际项目中 args 的键必须与你自己组件声明的 prop 名保持一致import type { Meta, StoryObj } from storybook/vue3-vite; import Button from ./Button.vue; const meta { component: Button, } satisfies Metatypeof Button; export default meta; type Story StoryObjtypeof meta; export const Primary: Story { render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { background: #ff0, label: Button, }, }; export const Secondary: Story { render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { ...Primary.args, label: , }, }; export const Tertiary: Story { render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { ...Primary.args, label: , }, };Vue 的 CSF Next TS 变体renderervue与languagets一组、languagejs一组在参数名上略有差异正文完整保留两份结构一致preview.meta({ component }) 对每个 story 调meta.story({ ... })其中render函数的写法与 CSF 3 完全相同只是复用基线参数改用Primary.input.argsimport preview from ../.storybook/preview; import Button from ./Button.vue; const meta preview.meta({ component: Button, }); /* * Render functions are a framework specific feature to allow you control on how the component renders. */ export const Primary meta.story({ render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { background: #ff0, label: Button, }, }); export const Secondary meta.story({ render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { ...Primary.input.args, label: , }, }); export const Tertiary meta.story({ render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { ...Primary.input.args, label: , }, });import preview from ../.storybook/preview; import Button from ./Button.vue; const meta preview.meta({ component: Button, }); export const Primary meta.story({ render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { backgroundColor: #ff0, label: Button, }, }); export const Secondary meta.story({ render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { ...Primary.input.args, label: , }, }); export const Tertiary meta.story({ render: (args) ({ components: { Button }, setup() { return { args }; }, template: Button v-bindargs /, }), args: { ...Primary.input.args, label: , }, });2.7 Web ComponentsWeb Components 渲染器里meta 的component传入的不是构造函数而是自定义元素标签字符串如demo-buttonexport default { component: demo-button, }; export const Primary { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary { args: { ...Primary.args, label: , }, }; export const Tertiary { args: { ...Primary.args, label: , }, };TS 变体为无参泛型Meta/StoryObjimport type { Meta, StoryObj } from storybook/web-components-vite; const meta: Meta { component: demo-button, }; export default meta; type Story StoryObj; export const Primary: Story { args: { backgroundColor: #ff0, label: Button, }, }; export const Secondary: Story { args: { ...Primary.args, label: , }, }; export const Tertiary: Story { args: { ...Primary.args, label: , }, };Web Components 的 CSF Next 变体JS/TS 文件体一致使用preview.metameta.story复用改为Primary.input.argsimport preview from ../.storybook/preview; const meta preview.meta({ component: demo-button, }); export const Primary meta.story({ args: { backgroundColor: #ff0, label: Button, }, }); export const Secondary meta.story({ args: { ...Primary.input.args, label: , }, }); export const Tertiary meta.story({ args: { ...Primary.input.args, label: , }, });三、复用写法变化点CSF 3 的...Primary.args与 CSF Next 的...Primary.input.args对照上面的代码可以看到同一套“继承 覆盖”逻辑在两种格式下写法不同CSF 3story 就是普通具名导出的对象因此直接读Primary.args即可展开复用CSF Next预览故事由meta.story()工厂函数生成story 的属性集合被收纳进composed属性该命名表示其值由 “story 本身 组件 meta preview 全局配置” 组合而成。CSF Next 仍兼容直接读取Story.args但官方已将其标记为deprecated推荐改用Story.extend或在需要拿到“原始输入”时读取Story.input.args见 docs/api/csf/csf-next.mdx#L454-L492。例如要派生出“禁用的 Primary”CSF Next 更推荐这种链式写法export const PrimaryDisabled Primary.extend({ args: { disabled: true, }, });.extend对属性采用智能合并args浅合并、parameters深合并数组整体替换、decorators/tags拼接。需要指出的是CSF Next 目前是preview特性只支持 React、Vue、Angular、Web Components 项目本文 2.1/2.3/2.6/2.7 中出现标注的代码块均属此列并且不允许在同一文件内混用故事格式详见 docs/api/csf/csf-next.mdx#L554-L560 的 FAQ。四、为什么 args 能被“实时编辑”运行时机制与 Controls理解了三态代码后值得追问一层为什么这类故事不是“死代码”从 docs/writing-stories/args.mdx 可以归纳出以下几点运行时事实args 可在 story、component、global 三个层级定义。本文所有代码都属于 story 级 args——只作用于所在故事你还可以在 metadefault export里定义 component 级 args让某组默认值作用于该组件的全部故事或在preview.*的默认导出里定义 global 级 args。层级越低优先级越高低层级可覆盖高层级。渲染与重渲染默认情况下story 渲染 meta 中声明的component并把 args 传入当某个 arg 的值变化时组件随之重新渲染这是 UI 交互与调试的基础。Controls 面板的现场编辑每个来自 story/args 的属性都会进入 Controls 面板团队可以在界面上拖动/输入来动态改变组件、寻找边界用例甚至调整参数后另存为新的 story。这也是原文档强调“spread args onto the Button component确保 Controls 等特性正常工作”的原因——如果你的自定义render函数没有把 args 透传给组件Controls 的修改就无法生效。自定义 render 的 context 参数render函数与 Svelte 的 template snippet 还会收到第二个context参数内含parameters、globals等信息而 args 是第一参数约定俗成解构为(args)。URL 亦可覆盖 args浏览器地址栏可通过?path/story/...argssize:100这样的键值对覆盖当前故事的初始 args键值限字母数字、空格、下划线与短横线特殊值用!null/!undefined前缀表达日期、颜色有专门编码这在 docs/writing-stories/args.mdx#L91-L107 有完整的格式说明。五、从“同一组件多状态”到“跨组件组合 args”三态 Button 只是起点。原文档接着演示了更进一步的组合能力既然 args 是纯对象就可以跨 story 文件导入复用。典型场景是组合组件composite component例如由若干Button组成的ButtonGroup// 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 { ButtonGroup } from ../ButtonGroup; // Imports the Button stories import * as ButtonStories from ./Button.stories; const meta { component: ButtonGroup, } satisfies Metatypeof ButtonGroup; export default meta; type Story StoryObjtypeof meta; export const Pair: Story { args: { buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }], orientation: horizontal, }, };完整的多框架等价写法见 button-group-story.md其中 CSF Next 变体对应地使用ButtonStories.Primary.input.args。这种模式的工程价值在原文档中被明确点出当Button的签名发生变化时只需要改Button.stories中的数据定义ButtonGroup的故事会自动同步更新从而让你把数据定义复用到整个组件层级上故事的可维护性随之提升见 docs/writing-stories/index.mdx#L279-L285。此外这类 args 也可以被组合进更上层的页面故事page story用于演示完整界面其思路与 page-story.md 一致。六、实践要点小结对照 button-story-using-args.md 的完整代码可以提炼出几条可直接落地的经验用 args 而非复制 JSX/模板来写状态同一组件尽量写成一个基线 story 若干通过展开派生的 story避免重复。渲染器的差异只在“透传”环节React / Solid / Angular / Web Components 通常无需render默认把 args 传给组件HTML 与 Vue 需要显式rendercreateButton(args)或Button v-bindargs /且透传是 Controls 生效的前提。命名约定story 具名导出建议使用 UpperCamelCasePrimary、SecondaryStorybook 会基于导出名与 meta 生成侧边栏层级。meta 是锚点默认导出或 CSF Next 的preview.meta中的component决定了自动标题推导与故事归属title可省略。按项目选格式常规项目使用 CSF 3typescript 工程配合satisfies Meta...StoryObjtypeof meta可获得完整类型推导CSF Next 当前属于 preview 特性提供全链路工厂类型安全但 API 仍在演进升级前请阅读 docs/api/csf/csf-next.mdx 的迁移指南Svelte 项目则可二选一使用 Svelte CSF 或标准 CSF同一文件内不可混用。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表