首页
/ 深入掌握 Storybook 的 webpackFinal:在 main 配置中定制 Webpack 构建的完整指南

深入掌握 Storybook 的 webpackFinal:在 main 配置中定制 Webpack 构建的完整指南

2026-09-07 15:59:20作者:晏闻田Solitary

导读

webpackFinal 是 Storybook 主配置(.storybook/main.js|ts)中用于定制 Webpack 构建的关键字段。本文基于 Storybook 官方 API 文档与仓库内 builder-webpack5 源码,系统讲解该字段的类型签名、两份入参的用法、DEVELOPMENT/PRODUCTION 环境分支写法,以及它背后的 preset 管线实现原理,并给出模块别名、复用现有 Webpack 配置等可落地的实战片段。读完本文,你可以在使用 webpack 系 builder(如 react-webpack5nextjsangular 等)的项目中安全、精准地扩展 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-webpack5nextjsangular@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-webpack5nextjsangularserver-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。

务必保留配置对象中的 entryoutput 字段(详见后文"注意事项"),否则 preview 页面将无法正确产出。

参数二:options(WebpackOptions)

官方类型定义如下:

type Options = { configType?: 'DEVELOPMENT' | 'PRODUCTION' }

官方文档明确指出:还有其它难以逐一记录的选项(例如源码实践中常见的 configDirpresetsfeatures 等),建议直接 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.tsconst 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;
}

关键点可以拆成三步理解:

  1. 先生成默认配置:若 core.disableWebpackDefaults 未开启(默认不开启),Storybook 会调用 createDefaultWebpackConfig 生成一套内置默认配置,它已覆盖 MDX/CSF 解析、静态资源、JSON 导入、.ejs 模板等日常能力;
  2. 再叠加 addon 与你的钩子presets.apply('webpackFinal', ...) 会按注册顺序把内置 preset、各 addon 暴露的 webpackFinal 以及你自己在 .storybook/main.js 中写的 webpackFinal 串联起来,后一个拿到前一个的返回值继续加工——所以你写在 main 里的钩子位于链条末尾,优先级最高;
  3. 可选的全权接管模式:若 .storybook 目录下存在自定义 webpack 配置文件(通过 loadCustomWebpackConfig(configDir) 探测),且以函数形式导出,则会进入 full-control 模式,把上面两步的产物作为 config 传入你的函数,由你决定最终返回的配置。

仓库也把用户级钩子与 preset 链的关系体现在类型上:StorybookConfigWebpackStorybookConfig 中剔除了 webpackwebpackFinalfeatures 后重新声明(见 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 常用于定位项目根目录相关的配置文件

注意事项与最佳实践

  1. 预览区与管理区是两套独立的 Webpack 配置webpackFinal 只作用于渲染 stories 的 preview iframe;Storybook 自身的 UI(manager)走另一套配置,不受该钩子影响。因此文档允许你在极端情况下完全替换 config.module.rules,但要意识到范围仅限于故事渲染。官方说明见 configure/webpack.mdx 的 "Extending Storybook’s webpack config" 小节。
  2. 不要动 entryoutput。它们是 preview 页面装配的基础,随意覆盖会造成启动或构建异常。
  3. 不要直接覆写 config.plugins。preview 页面依赖 HtmlWebpackPlugin 生成 HTML;如果确有需要,应当采用"追加到数组中"或谨慎重建列表(官方文档指向相关 issue 讨论),参见 storybook-main-simplified-config.md
  4. 留意 .ejs 文件的处理。若你的自定义 loader 没有用 test 显式限定文件扩展名,需要手动把 .ejs 扩展名排除掉,以免干扰 Storybook 的 HTML 模板加载。
  5. 每次修改后务必返回配置对象:遗忘 return config 是初学者最常见的错误,会导致 Storybook 拿到 undefined 而崩溃。
  6. 使用正确的类型入口:CSF 3 中 webpackFinal 的参数类型为 Webpack 的 Configuration 与 Storybook 的 Options(见 types.tsStorybookConfigWebpack 对两个钩子签名的注释);CSF Next 风格则统一由 defineMain 提供类型推导。
  7. 仅对 webpack builder 生效nextjs-vitereact-vitevue3-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 源码深入验证。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388