news 2026/9/8 22:25:51

Storybook stories 配置完全指南:从 glob 数组到 StoriesSpecifier 的源码级解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Storybook stories 配置完全指南:从 glob 数组到 StoriesSpecifier 的源码级解析

Storybook stories 配置完全指南:从 glob 数组到 StoriesSpecifier 的源码级解析

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

stories是 Storybook 主配置文件(main.js / main.ts) 中必需的核心字段,它决定了 Storybook 从哪些位置加载 story 与文档文件、以怎样的顺序在侧边栏展示,以及在 monorepo、多包项目中如何精准圈定组件范围。本篇以仓库中 docs/api/main-config/main-config-stories.mdx 及其配套代码片段(如 main-config-stories.md、main-config-stories-with-object.md、main-config-stories-with-logic.md)为主体,结合 core 源码 中的类型定义,讲解四种配置形态与底层工作方式。读完你将能:写出覆盖任意目录结构的故事加载规则、控制侧边栏排序、用titlePrefix统一 monorepo 中各组件的自动标题,并理解为何某些“自定义逻辑”写法会降低 Storybook 的性能优化能力。

stories 配置的定位:声明故事文件在哪里

Storybook 的理念是让 story 文件与它所描述的组件“就近存放”,而不是集中在一个专门目录里。官方推荐的目录结构如下:

• └── components ├── Button.ts └── Button.stories.ts

stories字段的作用,就是告诉 Storybook 应该用哪些匹配规则去这些就近散落的文件中找出故事。在配置文件类型定义中,该字段的完整类型为:

stories: | (string | StoriesSpecifier)[] | async (list: (string | StoriesSpecifier)[]) => (string | StoriesSpecifier)[]

也就是说,它既可以是一个字符串数组(由 glob 或对象组成),也可以是一个接收默认列表并返回新列表的异步函数。从源码看,这一结构在 core-common.ts 中以stories: StoriesEntry[]的形式挂在完整配置类型上,而StoriesEntryStoriesSpecifier都定义在 indexer.ts:

export interface StoriesSpecifier { /** When auto-titling, what to prefix all generated titles with (default: '') */ titlePrefix?: string; /** Where to start looking for story files */ directory: string; files?: string; } export type StoriesEntry = string | StoriesSpecifier;

基础形态:用 glob 字符串定位故事文件

最常见的配置方式,是在.storybook/main.js/.storybook/main.ts中提供一个 glob 字符串数组。以 CSF 3 语法为例:

export default { // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. framework: '@storybook/your-framework', stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], };

对应 TypeScript 版本,可借助框架包导出的StorybookConfig类型获得完整的类型提示与校验:

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. import type { StorybookConfig } from '@storybook/your-framework'; const config: StorybookConfig = { framework: '@storybook/your-framework', stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], }; export default config;

拆解这条默认规则,需要理解四个部分:

片段含义
../src/glob 的基准目录。.storybook/main.js位于项目根目录下的.storybook文件夹,因此../src相对项目根定位到src目录
**/匹配任意层级的子目录,实现递归扫描
*.stories.限定文件名必须以.stories.结尾
@(js|jsx|mjs|ts|tsx)圆括号联合模式,匹配这五种 JS/TS 扩展名之一

注意这里的文件名匹配语法遵循picomatch@(...)!(...)+(...)等 glob 扩展),因此你可以自由改成团队自己的命名约定,比如*.test-story.@(ts|tsx)*.spec.stories.tsx。不过文档同时提醒:一些 addon 可能默认假定 Storybook 的命名约定,改动命名前应评估 autodocs、自动生成索引等配套能力是否受影响。事实上,文档中另一个姊妹条目 main-config-indexers.mdx 也以stories匹配出的文件集合作为自定义 indexer 处理的对象,可见该约定贯穿了整个索引构建链路。

CSF Next:通过 defineMain 获得更强类型推断

除了 CSF 3 的手写对象形态,仓库还提供了面向新语法的defineMain辅助函数(在文档中被标记为 CSF Next 实验形态)。它会基于你传入的框架包对stories等字段做更严格的类型推导:

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) import { defineMain } from '@storybook/your-framework/node'; export default defineMain({ framework: '@storybook/your-framework', stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], });

defineMain从各框架包的node子路径导出,不同框架对应的导入来源如下(对应文档片段中的各 renderer 版本):

rendererdefineMain导入路径framework字段
React 系@storybook/your-framework/node(替换为实际包,如react-vitenextjsnextjs-vite对应实际框架名
Vue@storybook/vue3-vite/node@storybook/vue3-vite
Angular@storybook/angular/node@storybook/angular
Web Components@storybook/web-components-vite/node@storybook/web-components-vite

用数组控制侧边栏的展示顺序

stories是一个数组时,故事按照数组中 glob 出现的先后顺序被加载,这直接决定了它们在侧边栏中的排列次序。若你想让 MDX 编写的文档页排在最前、普通故事跟在后面,可以这样组织:

export default { // Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. framework: '@storybook/your-framework', stories: [ '../src/**/*.mdx', // 👈 These will display first in the sidebar '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', // 👈 Followed by these ], };

该模式在 TypeScript / CSF Next 写法下与基础形态一致,只是把stories从单条字符串换成多条字符串构成的数组。利用这一顺序语义,你可以把“引导文档”、“设计规范”、“组件总览”等 MDX 页面稳定地排到组件故事之前。

进阶形态:用 StoriesSpecifier 对象管理 monorepo 与多目录

字符串 glob 适合“全局一条规则通吃”的场景;而当项目包含多个包(monorepo)、或不同目录需要不同的匹配规则与标题体系时,可以将数组元素替换为配置对象。该对象的类型即前文源码中的StoriesSpecifier,在 docs/api/main-config/main-config-stories.mdx 中的完整定义为:

{ directory: string; files?: string; titlePrefix?: string; }

例如,要从packages/components目录加载故事,可以这样配置:

// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. import type { StorybookConfig } from '@storybook/your-framework'; const config: StorybookConfig = { framework: '@storybook/your-framework', stories: [ { // 👇 Sets the directory containing your stories directory: '../packages/components', // 👇 Storybook will load all files that match this glob files: '*.stories.*', // 👇 Used when generating automatic titles for your stories titlePrefix: 'MyComponents', }, ], }; export default config;

三个字段的职责与默认值如下:

字段必填类型默认值说明
directorystring从哪里开始查找故事文件,相对于项目根目录
filesstring'**/*.@(mdx|stories.@(js|jsx|mjs|ts|tsx))'相对directory的 glob(不带开头的./),用于进一步过滤文件名
titlePrefixstring''启用自动标题(auto-title)时,作为所有生成标题的前缀

各字段的效果与源码保持一致:titlePrefix对应 indexer.ts 注释中的 “When auto-titling, what to prefix all generated titles with”;files对应 “a glob, relative to directory, no leading./”;directory对应 “Where to start looking for story files”。

从 indexer.ts 的注释可以看到,内部默认的文件匹配模式还会把mdx一并纳入 story 候选(即以stories.@(mdx|js|jsx|mjs|ts|tsx)为内部基准)——这与文档表格列出的对外默认值略有差异,但都表明files未设置时 .mdx 与各类脚本扩展名都会被识别。

当 Storybook 启动并读取到对象形态的规则后,它会在packages/components目录内查找所有带stories扩展名的文件,并根据目录层级为它们自动生成故事标题。加上titlePrefix后,这些标题会统一带上前缀(例如MyComponents/Button),这对于在侧边栏中为某个独立组件库、或某个业务模块划出一个清晰分组特别有用。需要注意的是StoriesSpecifier也可以与字符串 glob 混用在同一个数组中,从而精细控制哪些区域走默认规则、哪些区域走定制规则。

自定义实现:async 函数动态拼接故事列表

如果常规的 glob 与对象都无法表达你的加载策略(例如故事清单需要从外部数据源或运行时文件清单中计算),stories还允许写成异步函数,它接收 Storybook 解析好的默认列表list,你可以返回一个新的列表:

// 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 { StoriesEntry } from 'storybook/internal/types'; async function findStories(): Promise<StoriesEntry[]> { // your custom logic returns a list of files } const config: StorybookConfig = { framework: '@storybook/your-framework', stories: async (list: StoriesEntry[]) => [ ...list, // 👇 Add your found stories to the existing list of story files ...(await findStories()), ], }; export default config;

上述模式把“自研查找逻辑”的结果追加到默认规则找出的故事之后,适合在保留既有故事的基础上叠加由程序计算出的故事文件。函数形态同样适用于 CSF Next:

import type { StoriesEntry } 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'; async function findStories(): Promise<StoriesEntry[]> { // your custom logic returns a list of files } export default defineMain({ framework: '@storybook/your-framework', stories: async (list: StoriesEntry[]) => [ ...list, ...(await findStories()), ], });

自定义实现的代价:静态分析被打破

官方文档在此处特别加了警告:Storybook 现在会对配置文件做静态分析以提升性能,使用自定义实现可能破坏或降低这种能力。结合仓库源码可以印证这一点:构建故事索引时存在一条静态化的路径,例如 core-server 下的get-stories-paths-from-config.ts会直接从配置中提取路径,normalize-stories.ts(位于 common/utils)把stories规则归一化成带正则importPathMatcher的匹配器;而把stories改成“运行时才能知道结果”的函数,意味着这些静态分析步骤无法预先完成。因此,能用 glob 字符串或StoriesSpecifier对象表达的规则,应优先采用静态形态,仅在确有动态需求时才回退到 async 函数。

源码视角:一条 stories 规则如何变成文件匹配

要真正理解四种配置形态的关系,可以顺着核心类型与工具函数梳理底层链路:

  1. 类型层:indexer.ts 定义StoriesSpecifierStoriesEntry与归一化后的NormalizedStoriesSpecifier。后者在两者基础上补上importPathMatcher: RegExp,即把字符串 glob 编译成用于匹配“相对于当前工作目录的 importPath”的正则。
  2. 归一化层:normalize-stories.ts 负责把数组里混用的字符串与对象统一转成NormalizedStoriesSpecifier,填上filestitlePrefix等默认值。
  3. 索引构建层:配置经 presets/common-preset.ts 注入 core-server 后,由 StoryIndexGenerator.ts 配合各文件 indexer,把匹配到的每个文件转换为可供 UI 展示的故事索引条目;get-stories-paths-from-config.ts则用于在静态分析阶段直接枚举配置声明的故事路径。

因此,stories不只是“入口清单”,它是整个故事索引(Story Index)的起点。与之衔接的扩展点是indexers配置——如 main-config-indexers.mdx 所述:stories负责圈定候选文件集合,而 indexer 决定这些文件如何被解析为故事条目。如果你想为项目引入非标准的故事文件类型(比如自定义 DSL),通常是“在stories中加入匹配路径 + 注册对应 indexer”两件事同时进行。

小结与最佳实践

  • 默认的stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)']已能覆盖绝大多数单体项目,故事与组件就近放置是官方推荐的组织方式。
  • 需要调整侧边栏顺序时,把规则拆成多个 glob 按数组先后排列即可。
  • 遇到 monorepo、独立组件库或多命名空间时,优先使用StoriesSpecifier对象,用directory + files + titlePrefix精确圈定范围并统一标题前缀;titlePrefix与自动标题机制配套生效。
  • 确有动态清单需求才使用 async 函数形态,并注意它会绕过配置的静态分析优化。
  • 自定义命名约定前,先评估依赖默认*.stories.*命名的 addon 与索引能力是否受影响。

更完整的字段说明、类型定义与各框架写法,可继续查阅本仓库中的 main-config-stories.mdx 参考文档,以及实际驱动解析的 indexer.ts 与 normalize-stories.ts 源码。

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

AI互动故事中的生成式物品与背包设计:从数据结构到Agent实践

最近在做一个 AI 角色聊天 RPG 互动故事的产品原型&#xff0c;核心玩法很简单&#xff1a;用户扮演主角&#xff0c;AI 扮演各种角色&#xff0c;在对话里推进剧情。做到一半我发现一个特别要命的问题——如果角色在对话里给了玩家一瓶药水、一把钥匙&#xff0c;或者玩家捡到…

作者头像 李华
网站建设 2026/9/8 22:20:10

tiny11builder:给老电脑装精简 Windows 11 的最短路径

tiny11builder&#xff1a;给老电脑装精简 Windows 11 的最短路径 【免费下载链接】tiny11builder Scripts to build a trimmed-down Windows 11 image. 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny11builder 那台 2015 年的笔记本&#xff0c;8G 内存、128…

作者头像 李华
网站建设 2026/9/8 22:19:55

Isaac Sim与Isaac Lab实战指南:从环境搭建到具身智能RL训练

如果你上一篇文章已经通读&#xff0c;会知道我在梳理具身智能仿真器这片地图时&#xff0c;特意把NVIDIA这套组合单独拎了出来。今天这篇就围绕 Isaac Sim 和 Isaac Lab 展开&#xff0c;把这两兄弟在具身智能研发里各自扛什么活、怎么配合、从零怎么搭起来&#xff0c;以及我…

作者头像 李华
网站建设 2026/9/8 22:19:15

不用手写 RL 循环,5 分钟跑通 TRL 大模型强化学习对齐

不用手写 RL 循环&#xff0c;5 分钟跑通 TRL 大模型强化学习对齐 【免费下载链接】trl Train transformer language models with reinforcement learning. 项目地址: https://gitcode.com/GitHub_Trending/tr/trl 想给大模型做 RLHF&#xff08;基于人类反馈的强化学习…

作者头像 李华
网站建设 2026/9/8 22:18:04

五轴机械臂运动学分析全流程:MATLAB仿真与SolidWorks建模实战

简介&#xff1a;一套完整的五轴机械臂运动学分析学习资料&#xff0c;面向机器人方向工程技术人员、科研人员及高校学生&#xff0c;帮助系统掌握运动学建模、求解与仿真方法。资源共17个文件&#xff0c;压缩包仅1.62MB&#xff0c;涵盖SolidWorks三维模型&#xff08;12个零…

作者头像 李华