深入掌握 Storybook 的 webpackFinal:在 main 配置中定制 Webpack 构建的完整指南
导读
webpackFinal 是 Storybook 主配置(.storybook/main.js|ts)中用于定制 Webpack 构建的关键字段。本文基于 Storybook 官方 API 文档与仓库内 builder-webpack5 源码,系统讲解该字段的类型签名、两份入参的用法、DEVELOPMENT/PRODUCTION 环境分支写法,以及它背后的 preset 管线实现原理,并给出模块别名、复用现有 Webpack 配置等可落地的实战片段。读完本文,你可以在使用 webpack 系 builder(如 react-webpack5、nextjs、angular 等)的项目中安全、精准地扩展 Storybook 的打包能力,而不会误伤其内置默认配置。
webpackFinal 是什么
webpackFinal 属于 Storybook 的 main 配置(其余字段见 main-config 概览)。官方 API 文档 main-config-webpack-final.mdx 给出了它的类型签名:
type: async (config: Config, options: WebpackOptions) => Config
其作用是:在使用 webpack builder 时,定制 Storybook 的 Webpack 设置。也就是说,当项目的 framework 基于 webpack 构建(例如 @storybook/react-webpack5、nextjs、angular、@storybook/server-webpack5 等)而非 Vite 时,你就可以通过这个钩子读取 Storybook 内部生成好的 Webpack 配置对象,做增量修改,再原样返回。
它和 viteFinal 是一对"孪生"钩子:一个面向 webpack builder,一个面向 Vite builder,二者在同一份 main 配置中可以并存,Storybook 只会按当前框架实际使用的 builder 来执行对应的那一个(源码示例 之外的 advanced 示例参见 storybook-main-advanced-config-example.md)。
基础用法:最小可运行的 main 配置
官方代码片段 main-config-webpack-final.md 给出了完整的最小示例。我们按不同文件格式与配置风格逐一拆解。
风格一:CSF 3 下的 JavaScript(.storybook/main.js)
export default {
// Replace your-framework with the framework you are using, e.g. react-webpack5, nextjs, angular, etc.
framework: '@storybook/your-framework',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
webpackFinal: async (config, { configType }) => {
if (configType === 'DEVELOPMENT') {
// Modify config for development
}
if (configType === 'PRODUCTION') {
// Modify config for production
}
return config;
},
};
风格一:CSF 3 下的 TypeScript(.storybook/main.ts)
// Replace your-framework with the framework you are using, e.g. react-webpack5, nextjs, angular, 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)'],
webpackFinal: async (config, { configType }) => {
if (configType === 'DEVELOPMENT') {
// Modify config for development
}
if (configType === 'PRODUCTION') {
// Modify config for production
}
return config;
},
};
export default config;
风格二:CSF Next(实验性)下的 TypeScript(.storybook/main.ts,React)
CSF Next 配置风格要求从 @storybook/your-framework/node 中导入 defineMain 包裹整个配置:
// 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/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
webpackFinal: async (config, { configType }) => {
if (configType === 'DEVELOPMENT') {
// Modify config for development
}
if (configType === 'PRODUCTION') {
// Modify config for production
}
return config;
},
});
对应 JS 变体只是把 defineMain 用 ESM 的 import 语句引入、去掉类型标注即可:
// 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/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
webpackFinal: async (config, { configType }) => {
if (configType === 'DEVELOPMENT') {
// Modify config for development
}
if (configType === 'PRODUCTION') {
// Modify config for production
}
return config;
},
});
风格二:CSF Next 下的 Angular
只要框架的 node 入口支持,非 React 框架同样可以使用 defineMain,例如 Angular:
import { defineMain } from '@storybook/angular/node';
export default defineMain({
framework: '@storybook/angular',
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
webpackFinal: async (config, { configType }) => {
if (configType === 'DEVELOPMENT') {
// Modify config for development
}
if (configType === 'PRODUCTION') {
// Modify config for production
}
return config;
},
});
代码中
framework处需要把your-framework替换为你实际使用的框架包名,常见 webpack 系取值包括react-webpack5、nextjs、angular、server-webpack5等;stories同样按你的实际 glob 调整。若你的框架基于 Vite,则应当改用viteFinal。
两个入参的含义:config 与 options
webpackFinal 是一个异步函数,接收两个参数:
参数一:config
这是 Storybook 根据当前框架、addons、预设(presets)逐层加工后得到的完整 Webpack 配置对象(webpack.Configuration)。你应当以"原地读取 + 增量修改"的方式使用它:
- 追加 loader 到
config.module.rules; - 追加插件到
config.plugins; - 追加
config.resolve.alias、修改config.resolve.extensions; - 最后
return config,把修改后的配置交还给 Storybook。
务必保留配置对象中的 entry 与 output 字段(详见后文"注意事项"),否则 preview 页面将无法正确产出。
参数二:options(WebpackOptions)
官方类型定义如下:
type Options = { configType?: 'DEVELOPMENT' | 'PRODUCTION' }
官方文档明确指出:还有其它难以逐一记录的选项(例如源码实践中常见的 configDir、presets、features 等),建议直接 inspect 类型定义。在仓库内,这份类型定义真实存在于 builder-webpack5/src/types.ts:
webpack?: (config, options) => Configuration | Promise<Configuration>—— "在 Storybook 默认配置运行之后修改或返回自定义 Webpack 配置(主要由 addons 使用)";webpackFinal?: (config, options) => Configuration | Promise<Configuration>—— "在所有 addon 都执行完毕之后修改或返回自定义 Webpack 配置"。
这两者的先后顺序差异是理解 webpackFinal 的关键:webpack 阶段先于 addon 运行,而 webpackFinal 位于整个 preset 链的末尾,因此你的修改拥有最终决定权,可覆盖前面任何 addon 对配置的改动。
在 custom-webpack-preset.ts 中可以读到具体的使用场景——一个自定义 preset 内部以 configType 判断当前处于开发还是生产模式:
export async function webpackFinal(config: Configuration, options: Options) {
const previewConfigPath = findConfigFile('preview', options.configDir);
if (!previewConfigPath) {
return config;
}
// ...追加 mock loader、WebpackMockPlugin、注入 runtime 插件...
return config;
}
这证明 options 中实际还携带 configDir 等字段,二次参数在真实源码中远不止 configType 一个成员。
按环境区分处理:configType 的应用场景
最典型的需求是"开发模式与生产模式采用不同的编译策略",例如:
- 开发模式(
storybook dev)下启用更快的 source-map、关闭压缩或注入开发环境变量; - 生产模式(
storybook build,即build-storybook静态构建)下开启minimize、按需启用/关闭某些 loader。
示例中的 if (configType === 'DEVELOPMENT') { ... } 与 if (configType === 'PRODUCTION') { ... } 就是为这种差异化修改预留的分支。需要说明:官方文档的类型标注中 configType? 带有问号,说明该字段是可选的;不过 builder 内部正是靠它驱动多处逻辑——例如 iframe-webpack.config.ts 中 const isProd = configType === 'PRODUCTION' 决定是否压缩,并在注入的 CONFIG_TYPE 环境变量中沿用该值,preview-preset.ts 也用它判断是否需要注入运行时 mock。因此如果你希望自定义逻辑与 Storybook 内部行为保持一致,直接比较 configType 即可。
底层原理:webpackFinal 如何被调用
在 Storybook 中,main 配置其实是一个 preset 对象,webpackFinal 是它的一个 preset 属性。构建 preview 的 Webpack 配置时,builder-webpack5 会执行一套流水线,其核心逻辑位于 custom-webpack-preset.ts:
export async function webpack(config: Configuration, options: Options) {
const { configDir, configType, presets } = options;
const coreOptions = await presets.apply('core');
let defaultConfig = config;
if (!coreOptions?.disableWebpackDefaults) {
defaultConfig = await createDefaultWebpackConfig(config, options);
}
// ① 依次执行 addon / preset 链条上注册的 webpackFinal
const finalDefaultConfig = await presets.apply('webpackFinal', defaultConfig, options);
// ② 若用户提供了独立的 webpack 配置文件(full-control 模式)则以其结果为准
const customConfig = await loadCustomWebpackConfig(configDir);
if (typeof customConfig === 'function') {
logger.info('Loading custom Webpack config (full-control mode).');
return customConfig({ config: finalDefaultConfig, mode: configType });
}
logger.info('Using default Webpack5 setup');
return finalDefaultConfig;
}
关键点可以拆成三步理解:
- 先生成默认配置:若
core.disableWebpackDefaults未开启(默认不开启),Storybook 会调用createDefaultWebpackConfig生成一套内置默认配置,它已覆盖 MDX/CSF 解析、静态资源、JSON 导入、.ejs模板等日常能力; - 再叠加 addon 与你的钩子:
presets.apply('webpackFinal', ...)会按注册顺序把内置 preset、各 addon 暴露的webpackFinal以及你自己在.storybook/main.js中写的webpackFinal串联起来,后一个拿到前一个的返回值继续加工——所以你写在 main 里的钩子位于链条末尾,优先级最高; - 可选的全权接管模式:若
.storybook目录下存在自定义 webpack 配置文件(通过loadCustomWebpackConfig(configDir)探测),且以函数形式导出,则会进入 full-control 模式,把上面两步的产物作为config传入你的函数,由你决定最终返回的配置。
仓库也把用户级钩子与 preset 链的关系体现在类型上:StorybookConfigWebpack 从 StorybookConfig 中剔除了 webpack、webpackFinal、features 后重新声明(见 types.ts),即框架类型会约束你书写正确的钩子签名。
想直接查看"最终配置长什么样"?
官方 Webpack 配置文档(configure/webpack.mdx)建议,若你想了解默认配置的精确细节,可以运行带调试标志的命令,把最终解析出的配置打印出来:
# 开发模式
yarn storybook dev --debug-webpack
# 生产模式
yarn storybook build --debug-webpack
这对于排查"为什么我的 loader 没有生效"非常有用。
实战示例:在 webpackFinal 中完成常见定制
示例一:追加模块别名(alias)
官方片段 module-aliases-config.md 提供了 webpack 版本的模块 mock/别名写法。注意 Webpack 中精确匹配以 $ 结尾,且展开原有 resolve.alias 时不要覆盖内置别名:
import type { StorybookConfig } from '@storybook/your-framework';
const config: StorybookConfig = {
framework: '@storybook/your-framework', // 例如 nextjs / react-webpack5
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
// 👇 外部模块
lodash: import.meta.resolve('./lodash.mock'),
// 👇 内部模块($ 表示精确匹配,避免误伤子路径)
'@/api$': import.meta.resolve('./api.mock.ts'),
'@/app/actions$': import.meta.resolve('./app/actions.mock.ts'),
'@/lib/session$': import.meta.resolve('./lib/session.mock.ts'),
'@/lib/db$': import.meta.resolve('./lib/db.mock.ts'),
};
}
return config;
},
};
export default config;
示例二:接入 TypeScript 的路径映射
默认 Webpack 配置不会自动读取 tsconfig 里的路径别名(paths)。如果遇到模块解析失败,社区通行做法是引入 tsconfig-paths-webpack-plugin,在 webpackFinal 中把它追加进 config.resolve.plugins。相关思路与用法同样记录在官方文档 configure/webpack.mdx 的 "TypeScript Module Resolution" 小节及配套片段 storybook-main-ts-module-resolution.md 中。
示例三:复用项目现有的 Webpack 配置
若你的应用已有一份成熟 webpack.config.js(例如 Vue CLI、CRA 生成的项目),官方建议把应用配置导入 .storybook/main.js,再在 webpackFinal 中做合并——官方片段 storybook-main-using-existing-config.md 演示了用应用的 loaders 替换 Storybook 默认 loaders 的写法。这也是 preset 链"末尾覆写"特性的典型受益场景。
真实项目中的范例可参考 storybook-main-webpackfinal-example.md:CRA 类项目中,webpackFinal(config, { configDir }) 会先探测 react-scripts 是否安装,未安装则直接 return config(保留基础配置),已安装才调用 applyCRAWebpackConfig(config, configDir) 套用 CRA 的编译管线。从这里可以看出,options 中解构出来的 configDir 常用于定位项目根目录相关的配置文件。
注意事项与最佳实践
- 预览区与管理区是两套独立的 Webpack 配置。
webpackFinal只作用于渲染 stories 的 preview iframe;Storybook 自身的 UI(manager)走另一套配置,不受该钩子影响。因此文档允许你在极端情况下完全替换config.module.rules,但要意识到范围仅限于故事渲染。官方说明见 configure/webpack.mdx 的 "Extending Storybook’s webpack config" 小节。 - 不要动
entry与output。它们是 preview 页面装配的基础,随意覆盖会造成启动或构建异常。 - 不要直接覆写
config.plugins。preview 页面依赖HtmlWebpackPlugin生成 HTML;如果确有需要,应当采用"追加到数组中"或谨慎重建列表(官方文档指向相关 issue 讨论),参见 storybook-main-simplified-config.md。 - 留意
.ejs文件的处理。若你的自定义 loader 没有用test显式限定文件扩展名,需要手动把.ejs扩展名排除掉,以免干扰 Storybook 的 HTML 模板加载。 - 每次修改后务必返回配置对象:遗忘
return config是初学者最常见的错误,会导致 Storybook 拿到undefined而崩溃。 - 使用正确的类型入口:CSF 3 中
webpackFinal的参数类型为 Webpack 的Configuration与 Storybook 的Options(见 types.ts 中StorybookConfigWebpack对两个钩子签名的注释);CSF Next 风格则统一由defineMain提供类型推导。 - 仅对 webpack builder 生效:
nextjs-vite、react-vite、vue3-vite等基于 Vite 的框架不会执行webpackFinal,需要改用viteFinal。可在 main-config 概览 的配置项清单中核对两钩子的定位。
总结
webpackFinal 是 Storybook 面向 webpack builder 的"最后一公里"定制入口。它通过 preset 机制在默认配置与所有 addon 之后执行,让你能以最小侵入的方式扩展 preview 的打包能力。理解它的执行时机(webpack → addon → webpackFinal)、环境感知(configType)以及"增量修改并返回原对象"的使用范式,就能在开发模式、生产构建、别名解析、复用既有构建配置等场景下精准定制 Storybook。若需进一步掌握相关配置项的完整上下文,可继续阅读 main-config 概览 与 configure/webpack.mdx,并结合本文引用的 custom-webpack-preset.ts 源码深入验证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00