Storybook 复合组件文档化:用 `subcomponents` 把 List 与 ListItem 一起写进 Story 与 Autodocs

发布时间:2026/9/8 21:21:13
Storybook 复合组件文档化:用 `subcomponents` 把 List 与 ListItem 一起写进 Story 与 Autodocs Storybook 复合组件文档化用subcomponents把 List 与 ListItem 一起写进 Story 与 Autodocs【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook本篇技术指南围绕 Storybook 官方文档中的list-story-with-subcomponents代码示例展开讲解如何通过 meta 上的subcomponents属性把存在父子关系的组件如List与ListItem合并到同一份 Story/文档中并使其在 ArgsTableArgTypes 与 Controls中以“标签页”形式分别呈现各自属性。读完你将掌握subcomponents在 CSF 3 与 CSF Next 两种写法下的使用方式覆盖 Angular、React、Vue 3、Web Components、Solid、Svelte 六个渲染器并了解其底层实现原理与文档化层面的局限。subcomponents解决什么问题父子组件的联合文档化在真实组件库中很多组件天生就是“成对出现”的ButtonGroup与Button、List与ListItem、Page与页面内的多个区块组件设计上彼此配合才能工作。单独为它们各写一份 Story 往往无法描述“二者合体”的完整形态尤其当子组件并不适合被独立使用、只能作为父组件的插槽内容出现时。Storybook 的subcomponents属性正是为这种场景提供的元数据入口。它在仓库源码中有明确的类型定义addons.ts/** * Auxiliary sub-components that are part of the stories. * * Used by addons for automatic prop table generation and display of other component metadata. * * By defining them each component will have its tab in the args table. */ subcomponents?: Recordstring, ComponentType;也就是说subcomponents是一个“子组件名到组件对象”的映射表其用途在类型注释里写得很清楚供 addon 自动生成属性表并展示组件元数据每定义一个子组件它就会在 args 属性表中获得一个自己的标签页。在原文档stories-for-multiple-components.mdx中作者强调了一个关键使用前提当所记录的组件之间存在父子关系时可以使用subcomponents属性将它们一起记录。当子组件不打算单独使用、只作为父组件的一部分出现时这一能力尤其有用。一个完整的subcomponents示例List ListItem下面的核心代码来自仓库中的文档片段 list-story-with-subcomponents.md。该片段同时被两处文档引用stories-for-multiple-components.mdx讲解“为多个组件写 Story”时如何使用subcomponentsautodocs.mdx讲解 Autodocs“高级配置 → 记录多个组件”时的标准写法。示例的组件结构是父组件List通过插槽/children 接收一个或多个ListItem。二者都导入到故事文件中主组件放在component上ListItem则挂在subcomponents上import * as React from react; // 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 { List } from ./List; import { ListItem } from ./ListItem; const meta { component: List, subcomponents: { ListItem }, // Adds the ListItem component as a subcomponent } satisfies Metatypeof List; export default meta; type Story StoryObjtypeof meta; export const Empty: Story {}; export const OneItem: Story { render: (args) ( List {...args} ListItem / /List ), };要点拆解如下component定义主组件故事的标题、Args 传播、Controls 都以它为准subcomponents提供辅助组件{ ListItem }是{ ListItem: ListItem }的属性简写故事Empty渲染空列表OneItem通过自定义render把ListItem作为 children 塞进List验证两者的组合形态原片段中的 JS 版本与 TS 版本仅在类型标注上有别其余结构完全一致。全框架写法一览list-story-with-subcomponents片段在官方文档站点中以“渲染器标签页”形式承载了六个渲染器的等价写法其实现路径分别落在仓库的对应 framework 代码中。下面逐一展示各框架下结构差异最明显的版本。ReactJSX children 组合React 版本的核心特征是子组件以 JSX children 传入args通过展开运算符透传给父组件import * as React from react; import { List } from ./List; import { ListItem } from ./ListItem; export default { component: List, subcomponents: { ListItem }, // Adds the ListItem component as a subcomponent }; export const Empty {}; export const OneItem { render: (args) ( List {...args} ListItem / /List ), };AngularmoduleMetadata声明组件Angular 版本除component/subcomponents外还必须借助moduleMetadata装饰器把List、ListItem注册进测试模块并导入CommonModule才能让模板中的app-list、app-list-item被正确解析import { type Meta, type StoryObj, moduleMetadata } from storybook/angular; import { CommonModule } from angular/common; import { List } from ./list.component; import { ListItem } from ./list-item.component; const meta: MetaList { component: List, subcomponents: { ListItem }, // Adds the ListItem component as a subcomponent decorators: [ moduleMetadata({ declarations: [List, ListItem], imports: [CommonModule], }), ], }; export default meta; type Story StoryObjList; export const Empty: Story {}; export const OneItem: Story { args: {}, render: (args) ({ props: args, template: app-list app-list-item/app-list-item /app-list , }), };注意 Angular 的render返回{ props, template }结构模板里书写的是组件选择器而非 JSX这是与 React 最大的差异点。Vue 3render 函数中的components选项Vue 3 版本在render内返回components与template通过v-bindargs把参数绑定到List上import type { Meta, StoryObj } from storybook/vue3-vite; import List from ./List.vue; import ListItem from ./ListItem.vue; const meta { component: List, subcomponents: { ListItem }, // Adds the ListItem component as a subcomponent } satisfies Metatypeof List; export default meta; type Story StoryObjtypeof meta; export const Empty: Story { render: () ({ components: { List }, template: List /, }), }; export const OneItem: Story { render: (args) ({ components: { List, ListItem }, setup() { return { args }; }, template: List v-bindargsListItem //List, }), };Web Components子组件用标签字符串映射Web Components 渲染器中component与subcomponents的值不再是类组件而是自定义元素标签名字符串。这里子组件demo-list-item同样支持字符串形式映射import { html } from lit; export default { title: List, component: demo-list, subcomponents: { ListItem: demo-list-item }, // Adds the ListItem component as a subcomponent }; export const Empty {}; export const OneItem { render: () html demo-list demo-list-item/demo-list-item /demo-list , };Web Components 的 TS 版本从storybook/web-components-vite导入类型JS/TS 差异仅此一处。Solid提供显式titleSolid 的 JS 版本显式给出title: List并在注释中说明title属性是可选的若想了解如何基于文件路径生成自动标题可参考“configure story loading”相关文档import { List } from ./List; import { ListItem } from ./ListItem; export default { /* The title prop is optional. */ title: List, component: List, subcomponents: { ListItem }, // Adds the ListItem component as a subcomponent }; export const Empty {}; export const OneItem { render: (args) ( List {...args} ListItem / /List ), };其 TS 等价版本从storybook-solidjs-vite导入Meta/StoryObj类型并使用satisfies校验。SvelteSvelte CSF 的defineMetaSvelte 版本走的是storybook/addon-svelte-csf的defineMetaAPIcomponent与subcomponents一并传入故事用Story标签声明组合态通过{#snippet children(args)}实现而非render函数script module import { defineMeta } from storybook/addon-svelte-csf; import List from ./List.svelte; import ListItem from ./ListItem.svelte; const { Story } defineMeta({ component: List, subcomponents: { ListItem }, }); /script Story nameEmpty / Story nameOne Item {#snippet children(args)} List {...args} ListItem / /List {/snippet} /Story实验性写法CSF Next 下的preview.meta()除了 CSF 3 的export default meta写法原片段还以“CSF Next ”标签页形式提供了对应的实验性写法。其核心差异在于不再直接export default一个 meta 对象而是从.storybook/preview导入preview通过preview.meta({...})工厂创建 meta再用meta.story()声明故事。以 React TS 为例import * as React from react; import preview from ../.storybook/preview; import { List } from ./List; import { ListItem } from ./ListItem; const meta preview.meta({ component: List, subcomponents: { ListItem }, // Adds the ListItem component as a subcomponent }); export const Empty meta.story(); export const OneItem meta.story({ render: (args) ( List {...args} ListItem / /List ), });同样的preview.meta({ component, subcomponents })模式在原片段中还覆盖了 Angular、Vue 3、Web Components 三个渲染器区别仅在于render/template的写法与 Web Components 使用标签字符串。需要说明的是CSF Next 属于带“”标记的实验性特性写法与 API 形态在正式版本发布前仍可能调整生产项目建议先核对当前版本支持情况。加了subcomponents之后发生了什么ArgsTable 标签页机制当 meta 上声明subcomponents后ArgTypes 与 Controls 会为主组件之外再渲染一张子组件属性表。两张文档页面stories-for-multiple-components.mdx 与 autodocs.mdx都附上了同一张运行效果图ArgsTable 顶部出现List与ListItem两个标签页标签标题正是subcomponents对象的键名切换后可以看到List的someString必填类型string与someNumber默认0等属性信息。这套标签页机制的实现可以追溯到 addon-docs 的源码在 argTypesShared.ts 中extractSubcomponentArgTypes()会遍历subcomponents映射对每个子组件单独调用渲染器的 docgen 提取器extractArgTypes得到“子组件名 → StrictArgTypes”的映射渲染层则交给 TabbedArgsTable.tsx它把所有属性表按subcomponents的键渲染为TabsView标签页当只有一个条目时退化为普通ArgsTable。其中第一个标签页对应主组件其余标签页子组件会主动剥离“可控制性”相关 props——这与下述“子组件不带 Controls”的限制一一对应。仓库中还提供了针对性的官方示例故事可作为验证行为的手工测试样例ArgTypesWithSubcomponentsParameters.stories.tsx 与 ControlsWithSubcomponentsParameters.stories.tsx。文档化视角下的两条限制原文档stories-for-multiple-components.mdx明确指出subcomponents仅用于文档展示并附带两条硬性限制子组件的 argTypes 只能被自动推断不能手动定义或覆盖。原因在于子组件表的生成完全依赖渲染器的 docgen 提取器对应源码 argTypesShared.ts 中的extractComponentArgTypes/extractSubcomponentArgTypes实现Storybook 并不会为子组件执行用户自定义的argTypes合并逻辑。是否支持自动推断还取决于渲染器是否具备 docgen/类型推导能力相关机制见 arg-types.mdx 中关于 automatic argtype inference 的介绍。子组件属性表中没有可交互的 Controls。Controls 永远只作用于主组件的 args子组件标签页呈现的是只读属性说明。这与 controls.mdx 中“Controls 面板针对当前 story 主组件 args 生成控件”的设计一致也被 TabbedArgsTable.tsx 中“第一个标签页可控、其余标签页过滤控件相关 props”的实现印证。什么时候subcomponents不够用三条缓解路径当组合场景变得复杂、需要让子组件也受 args/Controls 驱动时stories-for-multiple-components.mdx 给出了三条互补技巧原片段在 docs/_snippets 中各有对应代码复用 story 定义list-story-reuse-data.md把ListItem故事里的 args 直接复用到List的render中减少重复数据。但它依然无法让子组件支持 Controls也无法在其他复合组件故事中复用。把 children 提升为 arglist-story-with-unchecked-children.md将渲染的子组件抽成一个childrenarg从而可被其他故事复用。该方法有明确注意事项——children与其他 args 一样必须可被 JSON 序列化因此应避免空值、若需经 Controls 调整应配合 mapping 等机制并对引入第三方库的组件保持谨慎。创建“模板组件”list-story-template.md做一个专门用于“生成复合组件故事”的模板组件把数据下沉为 args。这种方式初始化成本更高但换来的是每个 story 的 args 都能在 Controls 面板中被修改。在 Autodocs 中使用subcomponentsAutodocs 是这套能力的另一个主战场。autodocs.mdx 指出当一个组件库里的ButtonGroup和Button离开彼此就没有意义时Autodocs 允许你同时记录由component定义的主组件以及一个或多个关联subcomponents。效果与手写 CSF 一致——主组件与子组件会以标签页形式出现在ArgTypesDoc Block 中标签标题与subcomponents对象的键一一对应。若你希望为组件组定制更自由的组织方式例如并排对比多套参数组合官方建议改用 MDX 来编写文档页以获得对组件展示方式的完全控制。小结subcomponents是 Storybook 中“复合/父子组件文档化”的默认答案它用一行声明换来 ArgsTable 中按组件分标签页的属性展示底层由渲染器 docgen 推断 addon-docs 的TabbedArgsTable渲染共同实现。它适合“主组件可控、子组件作为插槽内容说明”的场景一旦你需要让子组件参与交互式 Controls 或做更自由的组合展示则应当转向 story 复用、childrenarg 或模板组件等方案甚至可以搭配 MDX 获得完全的文档编排自由。完整的、覆盖六个渲染器 × CSF 3/CSF Next 的逐条代码可随时回到仓库中的 list-story-with-subcomponents.md 查阅。【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考