尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Storybook 侧边栏 Single-Story Hoisting 单 Story 提升机制解析与跨框架写法

Storybook 侧边栏 Single-Story Hoisting 单 Story 提升机制解析与跨框架写法 Storybook 侧边栏 Single-Story Hoisting 单 Story 提升机制解析与跨框架写法Storybook 的侧边栏默认按title的/分段生成「分组 → 组件 → Story」三层结构。但当某个组件只包含一个与组件同名的 Story 时Storybook 会把这个 Story 自动提升hoist到组件所在位置直接以组件名显示在父分组下省去一层多余的展开节点。本文以仓库中的官方代码示例 docs/_snippets/button-story-hoisted.md 为核心骨架讲解单 Story 提升的触发条件、跨框架Angular / React / Vue / Web Components跨语法CSF 3 与 CSF Next的完整写法并结合侧边栏渲染源码剖析其底层实现帮助你掌握一套可复制、可验证的 Storybook 导航组织方案。一、什么是 Single-Story Hoisting在阅读代码示例前先看官方文档 docs/writing-stories/naming-components-and-hierarchy.mdx 中对这一行为的定义Single-story components即没有兄弟 Story 的组件当其display name与组件名称title的最后一段完全一致时会自动被提升hoisted在 UI 中替代其父级组件节点显示。结合本示例所用的分层标题Design System/Atoms/Button普通情况下侧边栏会渲染出Design System └── Atoms ├── Button ← 可展开的组件节点 │ └── Button ← 唯一的 Story └── Checkbox └── Checkbox开启提升后唯一的 Story 会向上顶替组件节点界面直接呈现为Design System └── Atoms ├── Button ← 单 Story 直接显示不可再展开 └── Checkbox下面的截图取自仓库文档资源 docs/_assets/writing-stories/naming-hierarchy-single-story-hoisting.png可以看到Atoms下的Button、Checkbox都以叶子节点形式直接展示二、提升的触发条件精确到源码仅凭直觉拼名字可能触发不了提升。仓库中侧边栏树的渲染实现在 code/core/src/manager/components/sidebar/Tree.tsx其中singleStoryComponentIds的计算逻辑给出了两条精确条件该节点必须是component类型而非 root/group/storychildren.length 1即组件下只有一个子节点子节点满足二选一子节点类型为docs纯文档组件——直接折叠子节点类型为story且subtype story——需进一步通过isStoryHoistable名称比对。名称比对函数定义在 code/core/src/manager/utils/tree.tsexport const removeNoiseFromName (storyName: string) storyName.replaceAll(/(\s|-|_)/gi, ); export const isStoryHoistable (storyName: string, componentName: string) removeNoiseFromName(storyName) removeNoiseFromName(componentName);也就是说Story 的展示名与组件名的比较会先剔除空格、连字符、下划线等噪声字符再要求字符串完全相等。这与 code/core/src/manager/utils/tree.test.js 中的单元测试相互印证// Very_Long-Button Story Name 清洗后为 VeryLongButtonStoryName与组件名相等 → true const output utils.isStoryHoistable(Very_Long-Button Story Name, VeryLongButtonStoryName); expect(output).toEqual(true); // Butto Story 清洗后为 ButtoStory与 ButtonStory 不等 → false const output2 utils.isStoryHoistable(Butto Story, ButtonStory); expect(output2).toEqual(false);确认可提升后Tree.tsx中的collapsedData会执行树重写把子 Story 的name替换为组件名、parent指向组件的父级、depth减一最终渲染时不再输出该组件节点Story 就出现在组件原本的位置上。三、编写满足提升条件的 Story 文件综上要写出可被提升的 Story 文件只需保证三点meta中声明分层title例如Design System/Atoms/Button——它是可选项省略时会依据文件路径生成自动标题详见 docs/configure/user-interface/sidebar-and-urls.mdxcomponent指向被测组件文件中只导出一个命名 Story且该 Story 的展示名与title最后一段组件名一致。由于 Story 导出名会被自动转成起始大写的展示名如myStory→My Story文件里通常直接使用组件同名导出Button或者通过Story.storyName ...把展示名显式改写成组件名。仓库以 docs/_snippets/button-story-hoisted.md 一份 snippet 同时维护了多种框架angular / react / vue / web-components与两种 CSF 语法的等价写法。下面分节给出完整示例。CSF 3 语法AngularTypeScriptimport type { Meta } from storybook/angular; import { Button as ButtonComponent } from ./button.component; const meta: MetaButtonComponent { // title 可选缺省时 Storybook 会依据文件位置生成自动标题 title: Design System/Atoms/Button, component: ButtonComponent, }; export default meta; type Story StoryObjButtonComponent; // 文件中唯一的命名导出且与组件同名 export const Button: Story {};通用 / CommonJavaScript适用于 React/Vue3 等渲染器import { Button as ButtonComponent } from ./Button; export default { // title 可选缺省时 Storybook 会依据文件位置生成自动标题 title: Design System/Atoms/Button, component: ButtonComponent, }; // 文件中唯一的命名导出且与组件同名 export const Button {};通用 / CommonTypeScript// 将 your-framework 替换为实际使用的框架如 react-vite、nextjs、vue3-vite 等 import type { Meta, StoryObj } from storybook/your-framework; import { Button as ButtonComponent } from ./Button; const meta { // title 可选缺省时 Storybook 会依据文件位置生成自动标题 title: Design System/Atoms/Button, component: ButtonComponent, } satisfies Metatypeof ButtonComponent; export default meta; type Story StoryObjtypeof meta; // 文件中唯一的命名导出且与组件同名 export const Button: Story {};Web ComponentsJavaScriptexport default { title: Design System/Atoms/Button, component: demo-button, }; // 文件中唯一的命名导出且与组件同名 export const Button {};Web ComponentsTypeScriptimport type { Meta, StoryObj } from storybook/web-components-vite; const meta: Meta { title: Design System/Atoms/Button, component: demo-component, }; export default meta; type Story StoryObj; // 文件中唯一的命名导出且与组件同名 export const Button: Story {};CSF Next实验性语法CSF Next 是仓库正在演进的下一代写法通过从.storybook/preview导入的preview.meta()与preview.story()声明元数据与 Story。以下各框架示例与上面的 CSF 3 写法语义一致。AngularTypeScriptimport preview from ../.storybook/preview; import { Button as ButtonComponent } from ./button.component; const meta preview.meta({ // title 可选缺省时 Storybook 会依据文件位置生成自动标题 title: Design System/Atoms/Button, component: ButtonComponent, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();ReactTypeScriptimport preview from ../.storybook/preview; import { Button as ButtonComponent } from ./Button; const meta preview.meta({ // title 可选缺省时 Storybook 会依据文件位置生成自动标题 title: Design System/Atoms/Button, component: ButtonComponent, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();ReactJavaScriptimport preview from ../.storybook/preview; import { Button as ButtonComponent } from ./Button; const meta preview.meta({ title: Design System/Atoms/Button, component: ButtonComponent, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();Vue 3TypeScriptimport preview from ../.storybook/preview; import ButtonComponent from ./Button.vue; const meta preview.meta({ // title 可选缺省时 Storybook 会依据文件位置生成自动标题 title: Design System/Atoms/Button, component: ButtonComponent, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();Vue 3JavaScriptimport preview from ../.storybook/preview; import ButtonComponent from ./Button.vue; const meta preview.meta({ title: Design System/Atoms/Button, component: ButtonComponent, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();Web ComponentsTypeScriptimport preview from ../.storybook/preview; const meta preview.meta({ title: Design System/Atoms/Button, component: demo-component, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();Web ComponentsJavaScriptimport preview from ../.storybook/preview; const meta preview.meta({ title: Design System/Atoms/Button, component: demo-button, }); // 文件中唯一的命名导出且与组件同名 export const Button meta.story();四、常见不生效原因与规避建议结合文档描述与源码逻辑以下场景不会发生提升需要逐一排查存在多个命名 Story只要组件下有多于一个子节点children.length ! 1提升即失效。此时应回到分组展开模式参考 docs/_snippets/button-story-grouped.md 与 docs/_snippets/checkbox-story-grouped.md 的写法。Story 展示名与组件名不一致Story 导出会自动起始大写例如导出primary得到展示名Primary。若组件名为Button而 Story 名为Primary比对失败可通过Primary.storyName Button覆盖展示名来对齐。若组件名自身含空格/连字符/下划线分隔词如VeryLongButtonStoryName由于比较前会剔除噪声字符Very_Long-Button Story Name这类名称也能通过比对——这一点在 code/core/src/manager/utils/tree.test.js 中有明确测试覆盖。组件下只有一个自动文档docs节点这是独立的折叠路径仅含docs子节点的组件会被收起展示属于另一类 UI 简化行为通常配合自动文档使用参见 docs/writing-docs/autodocs.mdx。另外需要注意本示例与文档中Single-story hoisting一节均不包含 Svelte 渲染器该章节对 Svelte 使用条件排除Svelte CSF 有自己独立的 Story 声明方式不要把上面的写法直接照搬到.stories.svelte。五、在更大层级组织中的定位Single-Story Hoisting 是 Storybook 侧边栏层级组织隐式 vs 显式命名的一部分隐式组织依赖 Story 文件物理位置自动生成标题显式组织用title显式指定位置并用/做分组本示例即属此类Roots默认最顶层分组以Root形式展示大写、不可展开可在 UI 配置中关闭该行为。当 Storybook 组件规模较大时官方建议按文件层级命名组件让侧边栏层级与文件系统保持一致当组件只有一个同名 Story 时则让提升机制自动替你收起一层最终在 docs/writing-stories/naming-components-and-hierarchy.mdx 所述的五层结构Category → Folder → Component → Docs → Story里把Story精确地放到Component的位置上。六、小结Single-Story Hoisting 的实现与使用可以浓缩为三句话两个条件缺一不可组件下只有一个 Story 子节点且其展示名清洗空格/连字符/下划线后与组件名相等代码上只需约定在meta中声明分层title文件中仅导出一个与组件同名的 StoryCSF 3 写export const Button: Story {}CSF Next 写export const Button meta.story()行为由渲染层保证Tree.tsx在渲染前先筛出可折叠组件、再重写节点关系因此你看到的是一个无需手动配置、自动生效的侧边栏优化。围绕本文的完整多框架、双语法的代码骨架均可在 docs/_snippets/button-story-hoisted.md 中按需取用配合 code/core/src/manager/components/sidebar/Tree.tsx、code/core/src/manager/utils/tree.ts 与 code/core/src/manager/utils/tree.test.js 即可验证其行为与边界条件。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表