ARTICLE DETAIL

资讯详情

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

Storybook Args 组合实战:为组合式页面复用子组件 Stories 数据(全框架代码解析)

Storybook Args 组合实战:为组合式页面复用子组件 Stories 数据(全框架代码解析) Storybook Args 组合实战为组合式页面复用子组件 Stories 数据全框架代码解析本篇指南以 page-story-with-args-composition.md 代码片段为骨架解析 Storybook 中Args 组合args composition这一页面级 Story 编写模式当页面Screen由 PageLayout、DocumentHeader、DocumentList 等多个子组件拼装而成时如何通过命名空间导入子组件的 Stories直接把user、document、subdocuments等 args 组合进页面 Story。读完你将掌握 CSF 3、Svelte CSF、CSF Next 三种格式下的完整写法与.args/.input.args的取值差异。场景与收益为什么页面级 Story 需要 Args 组合在 Storybook 官方文档的 Building pages and screens构建页面与屏幕 一节中官方给出了两种构建页面的路径纯展示型页面Pure presentational pages从原子组件一路组合到屏幕级组件都保持无副作用、纯 props 驱动把网络请求、Context、浏览器环境等connected逻辑全部收敛到应用外层的一个包装组件中对 connected 组件做 Mock见 Mocking imports 与 Mocking network requests。本文讨论的Args 组合属于第 1 条路线下的关键技巧。官方文档build-pages-with-storybook.mdx 中的Args composition for presentational screens小节给出了一条经验法则当你用这种方式构建屏幕时复合组件的输入通常是它渲染的各子组件输入的叠加。比如你的屏幕渲染了一个页面布局包含当前用户信息、一个页头描述正在查看的文档和一个列表子文档列表那么屏幕的输入大概率就是user、document、subdocuments这三项。正是这种页面 props 子组件 props 的并集的结构让把子组件的 Story 数据直接组合成页面 Story成为自然且高效的写法。官方特别强调其收益对应 关联 Snippet 在 build-pages-with-storybook.mdx 中的上下文说明当各子组件导出了一组复杂多样的 Stories时页面 Story 可以按需挑拣拼出真实场景而不必重复书写数据通过复用数据将维护成本压到最低遵循DRYDont-Repeat-Yourself原则——子组件 Story 里的样例数据只有一份真源single source of truth。前置基础Args 的对象语义与三层作用域Args 组合的本质是把 args 当作普通的、可序列化的 JavaScript 对象去引用与合并。在 Args 参考文档中定义了三个层级层级定义位置作用范围Story args某个 Story 对象上的args键CSF 3/Svelte CSF 中即Story组件或meta.story的args仅作用于该 StoryComponent argsdefault export上的argsSvelte CSF 中是defineMeta的args该组件所有 Story可被单个 Story 覆盖Global args.storybook/preview.*默认导出上的args全部组件的 Story三个层级的 args 在渲染时按 Story → Component → Global 的优先级逐层合并。args对象是 JSON 可序列化的、键为字符串的对象可传给任意框架的 props/slots/inputs这正是它可以被import * as PageLayout from ./PageLayout.stories这种普通 JS 导入跨文件引用的前提——Story 文件本身只是导出普通对象的模块。当 arg 值变化时组件会重新渲染这也是 Controls 等 Addon 能够所见即所得地实时编辑页面的基础只要页面 Story 的 args 都被显式声明Control 面板就能完整驱动整个屏幕。示例页面DocumentScreen 的纯展示式实现为了让Args 组合有具体的落点simple-page-implementation.md 给出了配套的纯展示组件实现。以 React TypeScript 变体为例页面的职责就是把三个 props 依次透传给三个子组件import PageLayout from ./PageLayout; import Document from ./Document; import SubDocuments from ./SubDocuments; import DocumentHeader from ./DocumentHeader; import DocumentList from ./DocumentList; export interface DocumentScreenProps { user?: {}; document?: Document; subdocuments?: SubDocuments[]; } export function DocumentScreen({ user, document, subdocuments }: DocumentScreenProps) { return ( PageLayout user{user} DocumentHeader document{document} / DocumentList documents{subdocuments} / /PageLayout ); }注意这里映射关系上的一点小设计页面 props 叫subdocuments而传给DocumentList时用的是其documentsprop。子组件 props 与页面 props 并不一定同名这正是后面组合 args 时需要小心对齐字段的原因。其余框架Angular 模板、Vue、Svelte、Web Components/Lit的等价实现同样收录在该 Snippet 中Angular 版通过Input()声明user/document/subdocumentsWeb Components 版则通过单个data对象承载三者。Args 组合的核心套路命名空间导入 字段引用整个 关联 Snippet 展示的模式可以用三步概括命名空间导入子组件 Storiesimport * as PageLayout from ./PageLayout.stories。这样PageLayout变成包含meta与全部命名 Story 的对象命名空间按子组件 Story 的 args 对齐页面字段user取自PageLayout.Simple.args.userdocument取自DocumentHeader.Simple.args.documentsubdocuments取自DocumentList.Simple.args.documents注意这里引用的是子组件 Story 内部声明 props 时的真实字段名如documents得到一个新的、可独立渲染的页面 StorySimple。它不重复造数据只做引用与重组。这与 args.mdx 中面向单个组件内部复用的 args 组合参考 button-story-primary-composition.md用 ES2015 展开语法...args把某个 Story 的 args 摊开再覆写是同一机制的两种应用粒度组件内复用靠对象展开跨组件/页面级复用则靠命名空间导入 逐字段引用。下面把该 Snippet 的完整代码矩阵按语法体系整理呈现便于你按自己的技术栈直接取用。CSF 3按框架组织Angular / 通用 / Web ComponentsAngularCSF 3从storybook/angular导入类型import type { Meta, StoryObj } from storybook/angular; import { DocumentScreen } from ./your-page.component; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta: MetaDocumentScreen { component: DocumentScreen, }; export default meta; type Story StoryObjDocumentScreen; export const Simple: Story { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };通用 TSReact / Solid / Vue3 等替换your-framework// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. import type { Meta, StoryObj } from storybook/your-framework; import { DocumentScreen } from ./YourPage; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta { component: DocumentScreen, } satisfies Metatypeof DocumentScreen; export default meta; type Story StoryObjtypeof meta; export const Simple: Story { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };两个值得留意的类型细节satisfies Metatypeof DocumentScreen让meta在保留字面量类型的同时接受编译期校验type Story StoryObjtypeof meta使后续Simple.args能获得与页面 props 对齐的类型提示例如user缺失时会报错。通用 JSimport { DocumentScreen } from ./YourPage; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; export default { component: DocumentScreen, }; export const Simple { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };纯 JS 写法没有类型约束Simple就是一个普通对象但也因此保留了页面 args 子组件 Story args 的直接引用这一最朴素的对象语义。Web ComponentsCSF 3// Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; export default { component: demo-document-screen, }; export const Simple { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };import type { Meta, StoryObj } from storybook/web-components-vite; // Imports the required stories import PageLayout from ./PageLayout.stories; import DocumentHeader from ./DocumentHeader.stories; import DocumentList from ./DocumentList.stories; const meta: Meta { component: demo-document-screen, }; export default meta; type Story StoryObj; export const Simple: Story { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };Web Components 的差异在于component传入的是注册好的自定义元素名demo-document-screen与 simple-page-implementation.md 中customElements.define(demo-document-screen, DocumentScreen)对应。此外两个子 Snippet 的导入风格略不同——JS 版用命名空间导入import * asTS 版用默认导入import PageLayout from二者在 Storybook 的 CSF 解析中都能工作。SvelteSvelte CSF 与 CSF 3 两套体系Svelte 生态同时支持两套写法由storybook/addon-svelte-csf提供的原生 Svelte 模板语法.stories.svelte以及通用 CSF 3 的 JS/TS 写法。Svelte CSF原生模板语法script module import { defineMeta } from storybook/addon-svelte-csf; import DocumentScreen from ./YourPage.svelte; // Imports the required stories import * as PageLayout from ./PageLayout.stories.svelte; import * as DocumentHeader from ./DocumentHeader.stories.svelte; import * as DocumentList from ./DocumentList.stories.svelte; const { Story } defineMeta({ component: DocumentScreen, }); /script Story nameSimple args{{ user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }} /script module import { defineMeta } from storybook/addon-svelte-csf; import DocumentScreen from ./YourPage.svelte; // Imports the required stories import * as PageLayout from ./PageLayout.stories.svelte; import * as DocumentHeader from ./DocumentHeader.stories.svelte; import * as DocumentList from ./DocumentList.stories.svelte; const { Story } defineMeta({ component: DocumentScreen, }); /script Story nameSimple args{{ user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }} /Svelte CSF 有两点语法特性值得注意一是组件元数据通过defineMeta({ component })声明并解构出Story组件meta 不再走export default二是它引用的子组件故事文件也是.stories.svelteimport * as PageLayout from ./PageLayout.stories.svelte组合逻辑与 CSF 3 完全一致——每个 Story 仍然暴露args属性供外部引用。TS 与 JS 变体在此例中无差异。Svelte 的 CSF 3 写法import DocumentScreen from ./YourPage.svelte; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; export default { component: DocumentScreen, }; export const Simple { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };// Replace your-framework with svelte-vite or sveltekit import type { Meta, StoryObj } from storybook/your-framework; import DocumentScreen from ./YourPage.svelte; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta { component: DocumentScreen, } satisfies Metatypeof DocumentScreen; export default meta; type Story StoryObjtypeof meta; export const Simple: Story { args: { user: PageLayout.Simple.args.user, document: DocumentHeader.Simple.args.document, subdocuments: DocumentList.Simple.args.documents, }, };注意 TS 版文件头注释提示storybook/your-framework需替换为svelte-vite或sveltekit。使用 CSF 3 时引用的子故事文件后缀则回到.stories.stories.ts/.stories.js。CSF Next实验性工厂函数模式与.input.argsCSF Next 是 Storybook 正在演进的新一代 Component Story Format文档中以 CSF Next 标注属于 preview 阶段 API。它引入definePreview→preview.meta→meta.story的工厂函数链来获得全链路类型推导。在 Args 组合场景下它的差别只在一处引用子故事 args 时用的是PageLayout.Simple.input.args.user而不是PageLayout.Simple.args.user。为什么是.input.args在 CSF Next 中故事输出对象内部区分了input你直接传入工厂函数的原始注解比如你手写的args与composed由该 Story 其 meta 全局 preview 配置合成后的最终值。官方文档csf-next.mdx明确说明如需访问直接输入用Story.input如需访问合成结果用Story.composed。Snippet 在这里选择引用input.args——即子组件 Story 中直接声明的 args这对应 CSF 3 中Story.args的语义也是最符合复用故事作者显式数据意图的取值。从源码实现看csf-factories.tsCSF Next 的 Story 对象由defineStory构造input字段即原始StoryAnnotations而composed是惰性计算的——首次访问时才通过composeStory(input, meta.input, undefined, meta.preview.composed)把 story 注解、meta 注解与 preview 全局注解合并起来对应源码中get composed()内的compose()闭包。同一文件还实现了Story.extendargs 浅合并、parameters 深合并、decorators/tags 拼接见 csf-factories.ts相关合并行为在 csf-factories.test.ts 中有配套测试覆盖。以下是该 Snippet 中四个支持 CSF Next 的渲染器Angular / Web Components / React / Vue的完整示例。Angularimport preview from ../.storybook/preview; import { DocumentScreen } from ./your-page.component; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: DocumentScreen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });Web Componentsimport preview from ../.storybook/preview; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: demo-document-screen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });import preview from ../.storybook/preview; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: demo-document-screen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });Reactimport preview from ../.storybook/preview; import { DocumentScreen } from ./YourPage; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: DocumentScreen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });import preview from ../.storybook/preview; import { DocumentScreen } from ./YourPage; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: DocumentScreen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });Vueimport preview from ../.storybook/preview; import DocumentScreen from ./YourPage.vue; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: DocumentScreen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });import preview from ../.storybook/preview; import DocumentScreen from ./YourPage.vue; // Imports the required stories import * as PageLayout from ./PageLayout.stories; import * as DocumentHeader from ./DocumentHeader.stories; import * as DocumentList from ./DocumentList.stories; const meta preview.meta({ component: DocumentScreen, }); export const Simple meta.story({ args: { user: PageLayout.Simple.input.args.user, document: DocumentHeader.Simple.input.args.document, subdocuments: DocumentList.Simple.input.args.documents, }, });CSF Next 的共性特征在以上示例中非常明显import preview from ../.storybook/preview引入全局 preview 构造器preview.meta(...)返回的meta不需要作为 default export每个故事由meta.story({...})产出组件 props 类型由工厂链自动推导不再需要手写MetaX/StoryObjX。附带一提若想用绝对路径导入 preview 配置以避免移动文件时相对路径失效可通过package.json的imports子路径或构建器别名实现详见 csf-next.mdx。使用 Args 组合的注意点与取舍何时该用子组件故事丰富、页面场景需拼装官方文档build-pages-with-storybook.mdx指出当各个子组件导出了复杂的、各不相同的一组 Stories 时Args 组合的收益最大。你可以从每个子组件的多个故事里挑出合适的那一个例如用户未登录的 PageLayout、有 20 个子文档的 DocumentList组合成有现实意义的页面场景全程不重复书写一条数据。权衡与边界数据耦合是双刃剑页面 Story 直接引用子组件 Story 的 args意味着子组件故事数据变更会自动同步到所有引用它的页面故事——这正是 DRY 的价值反过来页面故事也无法独立于子组件故事数据而存在。若某个数据只在页面层级需要仍应直接在页面 Story 里声明字段名对齐组合时以子组件实际接收的 prop/slot/input 名为准上文subdocuments → documents就是反例式提醒以渲染层真实传参为验证标准若多数故事共享同一批 args应优先考虑把公共部分放到component 级 argsmeta.args而非在页面故事中逐个组合官方在 args.mdx 中有明确建议页面还有插槽/自定义渲染等复杂输入时Args 组合可继续延伸可以用 args 动态填充子组件见 page-story-slots.md也可以在 CSF 3 中通过args: { ...PageLayout.Simple.args, document: ... }式的展开覆盖局部字段参考 button-story-primary-composition.md。在测试与工具链中的延伸价值由于组合后的页面 Story 仍是args 完整、可序列化的普通故事它天然能接入 Storybook 的其余能力Controls 面板会列出user/document/subdocuments全部字段供可视化调试Story 同时可被便携式故事portable stories复用到测试框架中。CSF Next 下 Story 对象还额外暴露run()与composed等成员源码见 csf-factories.ts使页面故事也能作为组件测试的载体这也是该工厂模式在设计上与args 组合 复用一脉相承的体现。小结Args 组合把页面数据 子组件数据的并集这一结构映射为一行行args字段引用让页面级 Story 摆脱重复造数据的负担。无论你使用 CSF 3、Svelte CSF 还是实验性的 CSF Next核心思想一致——命名空间导入子故事、逐字段对齐页面 props、让数据只有一份真源唯一的语法差异是 CSF Next 中通过Story.input.args引用故事作者直接声明的 args。需要动手时请结合 page-story-with-args-composition.md 的多框架代码矩阵、配套的 页面实现 以及 CSF Next 参考 按需取用。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表