
Storybook Vite Builder.storybook/main.ts 配置与 viteFinal 钩子实战详解本文以 Storybook 官方文档片段 storybook-vite-builder-ts-configure 展示的标准main.ts配置模板为主体讲解在使用 Vite builder 时framework、stories与viteFinal三个核心字段各自的职责并结合 builder-vite 的源码说明viteFinal拿到的config到底是怎么拼装出来的。读完你可以直接在 React、Vue3、Web Components、Next.js 等 Vite 系框架下写出类型安全、可运行的 Storybook 配置并通过viteFinal安全地注入别名、插件与环境变量等定制项。main.ts 标准配置三个核心字段文档片段给出的通用模板如下CSF 3 风格framework占位为storybook/your-framework// .storybook/main.ts // Replace your-framework with the framework you are using, e.g. react-vite, nextjs-vite, vue3-vite, etc. import type { StorybookConfig } from storybook/your-framework; const config: StorybookConfig { framework: storybook/your-framework, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|mjs|ts|tsx)], async viteFinal(config, options) { // Add your configuration here return config; }, }; export default config;三个字段各司其职framework指定 renderer builder 的组合包如storybook/react-vite、storybook/nextjs-vite、storybook/vue3-vite。模板中刻意写成your-framework占位提醒读者按实际框架替换这是新手配置中最常见的错误点storiesglob 模式数组../src/**/*.mdx与../src/**/*.stories.(js|jsx|mjs|ts|tsx)覆盖了 MDX 文档页与 CSF 3 故事文件的常见扩展名viteFinal在 builder 把「你的项目 Vite 配置」与「Storybook 自身 Vite 配置」合并完成之后执行的最终调整钩子是定制 Vite 行为的推荐入口。两种声明风格StorybookConfig 类型 vs defineMain同一份文档片段提供了两套写法。经典的import type { StorybookConfig }方式如上新风格则从框架包的/node子路径导出defineMain辅助函数// .storybook/main.tsreact 为例CSF Next import { defineMain } from storybook/your-framework/node; export default defineMain({ framework: storybook/your-framework, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|mjs|ts|tsx)], async viteFinal(config, options) { // Add your configuration here return config; }, });片段中 Vue 与 Web Components 变体结构完全相同仅框架包名不同例如import { defineMain } from storybook/vue3-vite/node; export default defineMain({ framework: storybook/vue3-vite, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|mjs|ts|tsx)], async viteFinal(config, options) { return config; }, });import { defineMain } from storybook/web-components-vite/node; export default defineMain({ framework: storybook/web-components-vite, stories: [../src/**/*.mdx, ../src/**/*.stories.(js|jsx|mjs|ts|tsx)], async viteFinal(config, options) { return config; }, });两种方式在运行时等价差异只在类型推导defineMain以辅助函数形式给出完整的参数类型推断而StorybookConfig则通过显式类型标注达到同样目的。选择哪种取决于你当前 Storybook 版本支持的 API 形态两种模板中的viteFinal钩子本身没有区别。viteFinal 的类型契约从源码看它的签名viteFinal 是main.ts的可选字段其官方类型在 builder 源码中定义于 types.tsexport type ViteFinal ( config: InlineConfig, options: Options ) InlineConfig | PromiseInlineConfig; export type StorybookConfigVite { viteFinal?: ViteFinal; };要点入参config是 Vite 的InlineConfig即已经合并完成的完整配置对象钩子可以返回配置对象本身也可以返回 Promise因为签名允许async函数你可以await外部逻辑后再返回必须返回修改后的配置只改对象而不return不会生效第二个参数options中可用configType区分运行场景。API 文档 main-config-vite-final.mdx 声明其类型为{ configType?: DEVELOPMENT | PRODUCTION }据此可以在 dev server 与storybook build两个阶段注入不同的配置。从源码结构看builder 通过presets.apply(viteFinal, ...)这一预设机制调用你的钩子dev server 路径在 vite-server.ts 中执行const finalConfig await presets.apply(viteFinal, config, options)生产构建路径在 build.ts 中有同样的调用。也就是说无论是npm run storybook起开发服务还是构建静态产物你的viteFinal都会在最末端被调用一次。viteFinal 拿到的 config 是如何拼装的要正确定制必须先理解钩子收到的config的来源。核心逻辑在 vite-config.ts 的commonConfig函数中加载项目自身的 Vite 配置通过 Vite 的loadConfigFromFile读取你的vite.config.*加载根目录root默认为.storybook目录的父目录const projectRoot resolve(options.configDir, ..);剥离build属性源码注释明确说明用户配置中的build会被拆出来因为其中的部分选项可能破坏 Storybook 构建如果确实需要定制build.target等选项官方建议放在viteFinal里做const { config: { build: buildProperty undefined, ...userConfig } {} } (await loadConfigFromFile(configEnv, viteConfigPath, projectRoot, undefined, undefined, configLoader)) ?? {};其中buildProperty?.target会被保留到 builder 自己的build配置中透传。叠加 Storybook 自身的配置builder 侧的内置配置包括configFile: false防止二次加载你的配置文件、一套自有的插件链pluginConfig含核心插件、external globals、故事入口虚拟模块插件、webpack 兼容 stats 插件、root: projectRoot、以及base: ./支持部署到子目录const sbConfig: InlineConfig { configFile: false, plugins: await pluginConfig(options), root: projectRoot, base: ./, ...(options.cacheKey ? { cacheDir: resolvePathInStorybookCache(sb-vite, options.cacheKey) } : {}), build: { target: buildProperty?.target }, }; const config: ViteConfig mergeConfig(userConfig, sbConfig);mergeConfig(userConfig, sbConfig)用 Vite 官方提供的mergeConfig递归合并两份配置——插件数组会拼接、对象字段会深合并而不是简单覆盖。合并结果再经 builder 内部处理就是你viteFinal收到的第一个参数。这解释了 builder README 中「builder 会读取你的 vite.config.js但为了正常工作可能会改动其中一些选项」这句话的含义见 builder-vite README。实际定制viteFinal 的典型用法用 mergeConfig 递归合并而不是手写展开官方推荐用 Vite 自带的mergeConfig做增量修改避免手动展开时丢字段// .storybook/main.ts import { mergeConfig } from vite; export default { async viteFinal(config, { configType }) { // configType: DEVELOPMENT | PRODUCTION return mergeConfig(config, { resolve: { alias: { foo: bar }, }, }); }, };按环境区分 dev 与 build结合configType可以只在开发模式注入 mock、source map 相关插件或在生产构建中关闭某些调试插件例如export default { async viteFinal(config, { configType }) { if (configType DEVELOPMENT) { return mergeConfig(config, { server: { port: 6006 }, }); } return config; }, };allowedHosts 与子目录部署当 Storybook 部署在带自定义域名或子路径的场景下dev server 的 host 校验可能拦截请求。builder 在 input/iframe.html 中给出的提示语明确指引用户「重新以--host标志启动或用viteFinal手动配置 Vite 的 allowedHosts」这正是viteFinal钩子的典型用途export default { async viteFinal(config) { return mergeConfig(config, { server: { allowedHosts: [your-domain.com] }, }); }, };修改 build 相关选项由于用户的build属性会在合并前被拆出见上文第 2 点凡涉及build.outDir、build.rollupOptions等定制都应写在viteFinal中而不是依赖vite.config的build字段export default { async viteFinal(config) { return mergeConfig(config, { build: { outDir: dist/storybook, }, }); }, };当 vite.config 不在默认位置viteConfigPath 选项如果你的项目 Vite 配置不在工作目录根部比如 monorepo 中放在别处builder 提供viteConfigPathbuilder 选项显式指定路径。该选项定义在 types.ts 的BuilderOptions中export type BuilderOptions { /** Path to vite.config file, relative to process.cwd(). */ viteConfigPath?: string; /** * How Vite loads the config file. Equivalent to Vites --configLoader CLI flag and the * configLoader option of loadConfigFromFile. * * Requires Vite 6.1.0 or higher. On older Vite versions this option is silently ignored. */ configLoader?: bundle | runner | native; };在main.ts中的写法来自 builder-vite README// .storybook/main.mjs const config { framework: { name: storybook/react-vite, options: { builder: { viteConfigPath: .storybook/customViteConfig.js, }, }, }, }; export default config;注意两个约束viteConfigPath是相对process.cwd()的路径configLoader仅在 Vite 6.1.0 及以上生效旧版本会被静默忽略。工作目录与安全边界builder README 特别强调了工作目录约定builder-vite README默认启用 Vite 的server.fs.strict以增强安全性默认项目root为 Storybook 配置目录的父目录——与源码中const projectRoot resolve(options.configDir, ..)的推导完全一致如需覆盖root官方指引同样是「在viteFinal中修改」而不是引入其他机制。这也意味着 stories glob如../src/**/*.stories.(js|jsx|mjs|ts|tsx)的相对基准是.storybook目录本身而 Vite 的root基准是.storybook的父目录两者基准不同排查「配置生效但文件找不到」类问题时值得先核对。开发期与变更检测路径同样走 viteFinal从源码结构看viteFinal不只作用于普通 dev serverbuilder 的无头变更检测适配器change-detection-adapter在组装配置时复用了与 dev server 完全相同的「commonConfigviteFinal」流程并调用 Vite 的无服务resolveConfig来解析插件——见 headless.ts 中的options.presets.apply(viteFinal, config, options)及其注释。对应的测试 headless.test.ts 断言了「按 dev server 相同方式组装development 模式下的 commonConfig然后应用 viteFinal」。这一实现细节的价值在于你在viteFinal中做的任何配置别名、插件、环境变量前缀等在文件监听/变更检测等内部流程中也会保持一致不会因为走的是无头路径而丢失。小结main.ts的最小可运行形态是frameworkstoriesviteFinal是定制 Vite 行为的官方推荐入口其签名为(config: InlineConfig, options: Options) InlineConfig | PromiseInlineConfig声明风格可选经典StorybookConfig类型标注或defineMain辅助函数storybook/framework/node二者运行时等价钩子收到的configmergeConfig(你的 vite.config, Storybook 内置配置)其中用户的build字段被拆出、build.target被透传、base固定为./涉及build定制、allowedHosts、按configType分环境注入插件时一律在viteFinal中用mergeConfig返回修改后的配置vite.config不在默认位置时用 framework 的 builder 选项viteConfigPath指定configLoader选项要求 Vite 6.1.0。相关源码与文档入口builder-vite 实现、vite-config.ts、types.ts、vite-server.ts、build.ts、viteFinal API 文档、配置片段原文。创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考