ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Storybook 自定义 Indexer 中的标题规范化:使用 makeTitle 与 IndexInput.title 定制侧边栏名称

Storybook 自定义 Indexer 中的标题规范化:使用 makeTitle 与 IndexInput.title 定制侧边栏名称 Storybook 自定义 Indexer 中的标题规范化使用 makeTitle 与 IndexInput.title 定制侧边栏名称Storybook 通过experimental_indexers暴露了一套实验性的 Indexer API允许开发者把自定义文件格式如 JSON 数据、模板语言、URL 集合等转换为索引中的 story 条目。而makeTitle是这套 API 中最容易被误用的一环它是IndexerOptions提供的唯一工具函数用于把“用户自定义标题”统一包装成与默认命名行为一致的正式标题。本文以 docs/_snippets/main-config-indexers-title.md 所展示的combosIndexer为骨架讲解在自定义 Indexer 的IndexInput中应如何使用makeTitle生成title并深入到 Storybook 源码 中验证其底层调用链与自动命名规则读完即可在自己的.storybook/main.ts中写出命名风格统一的自定义 Indexer。背景Indexer 决定了 story 的标题从哪来在 Storybook 中Indexer 负责构建整个故事索引stories index——即全部 story 的清单及它们的元数据id、title、tags等该索引可在 Storybook 的/index.json路由下读取。这套 API 是“高级功能”它决定 Storybook 如何把文件解析为 story 条目你既可以控制用哪种语言书写 story也可以控制 story 内容从哪里来。自定义 Indexer 需要以函数形式注册到配置中函数返回包含已有 Indexer 在内的完整列表从而追加或替换默认 Indexerexport default { framework: storybook/your-framework, stories: [ ../src/**/*.mdx, ../src/**/*.stories.(js|jsx|mjs|ts|tsx), ], experimental_indexers: async (existingIndexers) { const customIndexer { test: /\.custom-stories\.[tj]sx?$/, createIndex: async (fileName) { // See API and examples below... }, }; return [...existingIndexers, customIndexer]; }, };请注意两个前提对应 main-config-indexers.mdx 的说明该功能处于实验阶段必须以StorybookConfig的experimental_indexers属性声明想让 Indexer 处理某个文件该文件必须被stories配置的 glob 覆盖到见 docs/_snippets/main-config-indexers.md 中“Make sure files to index are included instories”的注释。整个过程可概括为三步Storybook 依据stories找到与test正则匹配的文件 → 把文件交给你的createIndex函数 →createIndex返回的条目填充侧边栏与索引。其数据流如下图所示图片来自本仓库文档资源反映自定义 Indexer 在整体架构中的位置makeTitleIndexerOptions 中唯一的标题规范化入口在类型层面IndexerOptions定义在 code/core/src/types/modules/indexer.ts它只有一个成员export interface IndexerOptions { makeTitle: (userTitle?: string) string; }createIndex函数的签名同一文件 L56-L65为export type Indexer BaseIndexer { createIndex: (fileName: string, options: IndexerOptions) PromiseIndexInput[]; };makeTitle的语义接收一个用户提供的标题userTitle返回用于索引条目的规范化标题会展示在侧边栏中。如果用户不提供标题它就会基于文件名与路径自动生成一个标题。换句话说makeTitle是自定义 Indexer 与 Storybook 默认标题策略之间的“翻译层”。文档 main-config-indexers.mdx 对它的要求非常明确凡是显式指定title的索引条目必须通过makeTitle处理否则就会脱离 Storybook 的默认命名行为。从源文档继承一个为标题追加 “Custom” 后缀的完整 Indexer下面的combosIndexer就是 main-config-indexers-title.md 给出的完整示例它匹配所有.stories.[tj]sx文件从文件名中提取标题再交给makeTitle生成正式标题并追加Custom字样。首先是面向“CSF 3”时代配置风格的 TypeScript 版本defineMain/CSF Next 之外的经典写法// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. import type { StorybookConfig } from storybook/your-framework; import type { Indexer } from storybook/internal/types; const combosIndexer: Indexer { test: /\.stories\.[tj]sx?$/, createIndex: async (fileName, { makeTitle }) { // Grab title from fileName const title fileName.match(/\/(.*)\.stories/)[1]; // Read file and generate entries ... const entries []; return entries.map((entry) ({ type: story, // Use makeTitle to format the title title: ${makeTitle(title)} Custom, importPath: fileName, exportName: entry.name, })); }, }; const config: StorybookConfig { framework: storybook/your-framework, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|ts|tsx)], experimental_indexers: async (existingIndexers) [...existingIndexers, combosIndexer], }; export default config;若不想引入StorybookConfig类型标注也可使用无类型版本如源文档中对应 JS 变体const combosIndexer { test: /\.stories\.[tj]sx?$/, createIndex: async (fileName, { makeTitle }) { const title fileName.match(/\/(.*)\.stories/)[1]; // Read file and generate entries... let entries []; return entries.map((entry) ({ type: story, title: ${makeTitle(title)} Custom, importPath: fileName, exportName: entry.name, })); }, }; const config { framework: storybook/your-framework, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|ts|tsx)], experimental_indexers: async (existingIndexers) [...existingIndexers, combosIndexer], }; export default config;注意两点关键写法createIndex从IndexerOptions中解构出makeTitle生成的条目对象只携带四个字段type: story、title、importPath、exportName。其中title一定是makeTitle(...)的产物——这正是源文档示例要强调的“当指定title时必须使用makeTitle以沿用默认命名行为”。使用 CSF NextdefineMain风格时的写法差异同一逻辑在“CSF Next”实验性配置风格下只需把配置对象包进defineMain并从对应框架的/node入口导入。以 React 技术栈为例import type { Indexer } from storybook/internal/types; // Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) import { defineMain } from storybook/your-framework/node; const combosIndexer: Indexer { test: /\.stories\.[tj]sx?$/, createIndex: async (fileName, { makeTitle }) { const title fileName.match(/\/(.*)\.stories/)[1]; // Read file and generate entries ... const entries []; return entries.map((entry) ({ type: story, title: ${makeTitle(title)} Custom, importPath: fileName, exportName: entry.name, })); }, }; export default defineMain({ framework: storybook/your-framework, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|ts|tsx)], experimental_indexers: async (existingIndexers) [...existingIndexers, combosIndexer], });源文档还提供了 Vue、Angular、Web Components 技术栈的等价变体它们的createIndex实现完全相同仅framework与defineMain导入来源不同归纳如下源文档 main-config-indexers-title.md 的原始片段即按此表拆分可为 JS/TS、.storybook/main.js或.storybook/main.ts任意组合套用技术栈defineMain导入来源framework取值Vue 3 (Vite)storybook/vue3-vite/nodestorybook/vue3-viteAngularstorybook/angular/nodestorybook/angularWeb Components (Vite)storybook/web-components-vite/nodestorybook/web-components-vite其余框架通用占位storybook/your-framework/nodestorybook/your-framework如react-vite、nextjs、nextjs-vite注defineMain与“CSF Next”仍属实验性写法正式切换前建议关注 docs/api/main-config/main-config.mdx 与framework的配置演进说明。源码验证makeTitle在索引生成管线中的真实调用自定义 Indexer 的createIndex并不是在真空里运行的。Storybook 的服务端索引生成器 StoryIndexGenerator.ts 的extractStories方法是它真正的“宿主”。在该方法中为当前文件挑选匹配的 Indexerthis.options.indexers.find((ind) ind.test.exec(absolutePath))调用indexer.createIndex(absolutePath, { makeTitle: defaultMakeTitle })——也就是说makeTitle由 Storybook 注入它的默认实现是defaultMakeTitleL406-L413。defaultMakeTitle的核心逻辑const defaultMakeTitle (userTitle?: string) { const title userOrAutoTitleFromSpecifier(importPath, specifier, userTitle); invariant( title, makeTitle created an undefined title. This happens when the fileName doesnt match any specifier from main.js ); return title; };这解释了一个重要的运行时行为如果传给makeTitle的文件名不能匹配main.js中任何storiesspecifiermakeTitle会抛错消息为 “makeTitle created an undefined title…”。因此自定义 Indexer 的test正则必须与stories的 glob 覆盖范围一致否则标题无法生成。随后createIndex返回的每个条目都要经过归一化L446-L482名称input.name ?? storyNameFromExport(input.exportName)标题input.title ?? defaultMakeTitle()——不写title时会自动用defaultMakeTitle()兜底IDinput.__id ?? toId(input.metaId ?? title, storyNameFromExport(input.exportName))即 story 的 id 由title/metaId与exportName派生子类型input.subtype ?? story标签combineTags(...projectTags, ...(input.tags ?? []))。由此可以推断即便你完全省略titleStorybook 也会为索引条目生成“默认标题”。省略标题通常是最推荐的做法文档原话是 “Most of the time, you shouldnotspecify a title”。而一旦你显式书写title就必须通过makeTitle包装让自定义标题“继承”默认标题基于路径/前缀的规范化行为。makeTitle背后的自动命名规则makeTitle的无参自动命名最终落到 code/core/src/shared/story-index/autoTitle.ts 的userOrAutoTitleFromSpecifierL49-L84const normalizedFileName slash(String(fileName)); if (importPathMatcher.exec(normalizedFileName)) { if (!userTitle) { const suffix normalizedFileName.replace(directory, ); let parts pathJoin([titlePrefix, suffix]).split(/); parts sanitize(parts); return parts.join(/); } if (!titlePrefix) { return userTitle; } return pathJoin([titlePrefix, userTitle]); } return undefined;结合sanitizeL12-L33默认标题规则要点包括从文件相对路径推导层级标题目录分隔符映射为侧边栏标题分隔符剥离.stories/.story后缀以及最终扩展名例如components/Button/Button.stories.ts→components/Button/Button若末级文件名与父目录同名如button/button.stories.js则去重为buttonindex命名的文件会被视作目录聚合入口而被折叠StoriesSpecifier若带titlePrefix见 indexer.ts 中的titlePrefix?: string无论自动还是用户标题都会统一拼上前缀。这正是源文档建议“标题必须经makeTitle处理”的原因绕开makeTitle硬编码标题等于同时绕开了目录层级清理、重复目录去重、titlePrefix前缀等一系列既有行为最终可能导致侧边栏结构与同项目其他 story 风格冲突。实际效果与组合方式对源文档示例中的文件src/components/Combo/Combo.stories.tsx假设你的storiesglob 与之一致title fileName.match(/\/(.*)\.stories/)[1]提取出的中间值是Combo/Combo之类的原始片段makeTitle(title)返回规范化后的标题如Combo/Combo依路径与sanitize规则而定makeTitle(title) Custom最终让侧边栏出现“xxx Custom”分组。在编写自己的 Indexer 时可参考以下决策准则场景做法无需特殊分组沿用默认命名省略title让 Storybook 自动生成自动落到defaultMakeTitle()需要基于默认标题做轻微定制title: \${makeTitle(rawTitle)} Custom本示例的用法需要覆盖默认命名不应脱离makeTitle手工拼标题否则会失去titlePrefix、sanitize等默认行为进阶为什么很多自定义 Indexer 还需要“转译成 CSF”源文档明确提示除非你的 Indexer 做的是相对简单的事例如 sidebar-and-urls.mdx 中提到的“用不同命名约定索引 story”否则你很可能还需要把输入文件转译transpile成 CSF让浏览器端的 Storybook 能够真正读取并渲染这些 story。原因在于索引条目中的importPath最终必须解析到一个 CSF 文件。当源文件本身不是 CSF 时就需要构建插件Vite/Webpack 层面的 transform/loader把源格式改写为 CSF 再交给客户端。整体流程的第二张架构图如下该流程对应的正是 main-config-indexers.mdx 中“Transpiling to CSF”一节描述的六步UI 中用户导航到某个 story id → 浏览器按importPath请求 CSF 文件 → 服务端构建插件把源文件转译成 CSF 返回 → UI 读取 CSF、按exportName导入对应 story 并渲染。从源码类型看IndexInputindexer.ts L114-L161完整字段还包括subtype、rawComponentPath、metaId、name、tags、__id、__stats等其中只有type与exportName为必填。若你在自定义 Indexer 中额外设置了metaId或__id则被索引的 CSF 文件里的 meta / story 必须带有与之匹配的id/parameters.__id属性否则条目无法正确关联。同时注意Webpack 项目不支持自定义importPath只有 Vite 项目可以——Webpack 下需要把源文件转译成 CSF 并让importPath保持默认使用原始fileName。小结本仓库中的 main-config-indexers-title.md 虽只是一个代码片段却浓缩了自定义 Indexer 标题处理的两条铁律title能省则省——省略时 Storybook 会自动生成行为与内置 story 完全一致一旦显式给出title务必经由IndexerOptions.makeTitle包装以复用 autoTitle.ts 中基于 specifier、titlePrefix、目录清理的整套默认命名语义保证自定义条目在侧边栏中与原生 story 命名风格一致。从 StoryIndexGenerator.ts 的实现还可以看到makeTitle是服务端在调用createIndex时注入的标准能力并对无法匹配 specifier 的文件抛出明确错误。理解这层调用关系后你在用experimental_indexers处理 JSON fixture、模板语言或任意自定义源文件时就能自信地为每个条目生成正确、稳定、可被/index.json消费的标题。相关更完整的类型与字段说明可进一步阅读 main-config-indexers.mdx 与 indexer.ts 类型定义。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表