news 2026/9/18 15:44:47

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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下的ButtonCheckbox都以叶子节点形式直接展示:

二、提升的触发条件(精确到源码)

仅凭直觉拼名字可能触发不了提升。仓库中侧边栏树的渲染实现在 code/core/src/manager/components/sidebar/Tree.tsx,其中singleStoryComponentIds的计算逻辑给出了两条精确条件:

  1. 该节点必须是component类型(而非 root/group/story);
  2. children.length === 1,即组件下只有一个子节点;
  3. 子节点满足二选一
    • 子节点类型为docs(纯文档组件)——直接折叠;
    • 子节点类型为storysubtype === 'story'——需进一步通过isStoryHoistable名称比对。

名称比对函数定义在 code/core/src/manager/utils/tree.ts:

export 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 文件,只需保证三点:

  1. meta中声明分层title,例如'Design System/Atoms/Button'——它是可选项,省略时会依据文件路径生成自动标题(详见 docs/configure/user-interface/sidebar-and-urls.mdx);
  2. component指向被测组件;
  3. 文件中只导出一个命名 Story,且该 Story 的展示名与title最后一段(组件名)一致。

由于 Story 导出名会被自动转成"起始大写"的展示名(如myStoryMy Story),文件里通常直接使用组件同名导出(Button),或者通过Story.storyName = '...'把展示名显式改写成组件名。

仓库以 docs/_snippets/button-story-hoisted.md 一份 snippet 同时维护了多种框架(angular / react / vue / web-components)与两种 CSF 语法的等价写法。下面分节给出完整示例。

CSF 3 语法

Angular(TypeScript)

import type { Meta } from '@storybook/angular'; import { Button as ButtonComponent } from './button.component'; const meta: Meta<ButtonComponent> = { // title 可选,缺省时 Storybook 会依据文件位置生成自动标题 title: 'Design System/Atoms/Button', component: ButtonComponent, }; export default meta; type Story = StoryObj<ButtonComponent>; // 文件中唯一的命名导出,且与组件同名 export const Button: Story = {};

通用 / Common(JavaScript,适用于 React/Vue3 等渲染器)

import { Button as ButtonComponent } from './Button'; export default { // title 可选,缺省时 Storybook 会依据文件位置生成自动标题 title: 'Design System/Atoms/Button', component: ButtonComponent, }; // 文件中唯一的命名导出,且与组件同名 export const Button = {};

通用 / Common(TypeScript)

// 将 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 Meta<typeof ButtonComponent>; export default meta; type Story = StoryObj<typeof meta>; // 文件中唯一的命名导出,且与组件同名 export const Button: Story = {};

Web Components(JavaScript)

export default { title: 'Design System/Atoms/Button', component: 'demo-button', }; // 文件中唯一的命名导出,且与组件同名 export const Button = {};

Web Components(TypeScript)

import 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 写法语义一致。

Angular(TypeScript)

import 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();

React(TypeScript)

import 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();

React(JavaScript)

import 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 3(TypeScript)

import 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 3(JavaScript)

import 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 Components(TypeScript)

import preview from '../.storybook/preview'; const meta = preview.meta({ title: 'Design System/Atoms/Button', component: 'demo-component', }); // 文件中唯一的命名导出,且与组件同名 export const Button = meta.story();

Web Components(JavaScript)

import 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 的实现与使用可以浓缩为三句话:

  1. 两个条件缺一不可:组件下只有一个 Story 子节点,且其展示名(清洗空格/连字符/下划线后)与组件名相等;
  2. 代码上只需约定:在meta中声明分层title,文件中仅导出一个与组件同名的 Story,CSF 3 写export const Button: Story = {},CSF Next 写export const Button = meta.story()
  3. 行为由渲染层保证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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 15:44:04

通达信指数强度副图指标源码详解与参数调优

简介&#xff1a;通达信用户常有对比个股与大盘强弱的需求&#xff0c;这份《指数强度副图指标》教程文档正对应此类场景。内容从源码拆解入手&#xff0c;逐一说明 Z、X、T1&#xff5e;T3 等变量含义&#xff0c;讲解如何基于 N 日高低点区间计算个股相对强度&#xff0c;并绘…

作者头像 李华
网站建设 2026/9/18 15:43:27

macOS 27:看似微调,实则是架构级大变局?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 15:41:44

激光半导体光纤点式测温技术原理与工业落地

简介&#xff1a;本资源是一篇聚焦电力设备智能温控的工程技术论文&#xff0c;面向电气自动化、光纤传感及高压运维领域的工程师与高校研究人员&#xff0c;解决高压开关柜内电缆接头、动/静触点等关键部位因接触不良引发过热故障的实时监测难题。全文基于激光半导体材料折射率…

作者头像 李华