首页
/ Storybook 模块别名配置指南:用 viteFinal / webpackFinal 将依赖替换为 Mock 模块

Storybook 模块别名配置指南:用 viteFinal / webpackFinal 将依赖替换为 Mock 模块

2026-09-07 09:08:36作者:谭伦延

在 Storybook 中渲染或测试组件时,常常需要隔离其模块依赖(网络请求、session、第三方工具库等)。除了官方推荐、开箱即用的 Automocking(基于 sb.mock)之外,Storybook 还提供了一种名为 Builder Aliases(构建器别名) 的替代方案:直接在 .storybook/main.* 中修改构建器配置,让 Vite 或 Webpack 在解析模块时把「原模块路径」指向「mock 文件」。本文将以 docs/_snippets/module-aliases-config.md 中的完整配置为骨架,讲清楚这一方案的使用场景、配置细节、Vite/Webpack 的差异以及背后的实现原理。读完后,你可以为 Vite 与 Webpack 两类构建器、CSF 3 与实验性 CSF Next 两种配置风格,分别写出可运行的模块别名 mock 配置。

别名机制在整个 Mock 体系中的定位

mocking-modules.mdx 中,Storybook 把模块 mock 划分为三种手段,按推荐程度排序:

  1. Automocking:通过 storybook/test 中的 sb.mock() 注册模块,适用于 Vite 和 Webpack 构建器,配置成本最低;
  2. Subpath Imports:借助 Node 的 # 子路径导入与 package.jsonimports 字段实现按条件切换模块;
  3. Builder Aliases:当项目无法使用前两种方案时,直接修改构建器的模块解析别名(alias),在打包 Storybook 时把模块替换成 mock 文件。

本文的主角就是第三种。它与前两者的关键区别在于:mock 替换发生在模块解析(resolve)阶段,即构建器在「找文件」这一环节就把路径改写为 mock 文件,而不是在运行时拦截。因此它不依赖 sb.mockimports 条件导出,属于最底层、最通用也最「外科手术式」的手段。

这种写法需要你预先准备 mock 文件(例如与源文件同目录的 session.mock.ts),mock 文件通常用 storybook/testfn 工具包裹原函数,以便在 story 中控制行为并断言调用。参见 storybook-test-mock-file-example.md

import { fn } from 'storybook/test';
import * as actual from './session';

export * from './session';
export const getUserFromSession = fn(actual.getUserFromSession).mockName('getUserFromSession');

工作原理:别名是如何在 Storybook 的构建管线里生效的

之所以能在 .storybook/main.* 中改别名,是因为 Storybook 的构建器在产出最终配置前会暴露一个可编程的 hook:

也就是说,viteFinal / webpackFinal 都是异步的最终配置回调:Storybook 先装配好一份完整可用的构建配置(其中 config.resolve.alias 已包含默认别名),再调用你的回调做增量改写。你在回调里对 config.resolve.alias 展开合并,即用「对象展开」保留原有别名(...config.resolve?.alias),再叠加自己的路径映射。

正是由于别名最终进入真实构建器配置,Vite 侧甚至会把对象形式的 alias 归一化为数组形式 [{ find, replacement }](见 code/builders/builder-vite/src/change-detection-adapter/headless.ts 中的相关注释),最终作用于所有模块解析。所以:只要组件里出现与该 alias key 完全匹配的 import 语句,构建器就会把目标换成你指定的 mock 文件

Vite 构建器的别名配置

下面是 snippet 中 Vite(CSF 3)的完整配置。它同时演示了两类 mock 目标:外部 npm 包lodash)与项目内部模块@/api@/app/actions@/lib/session@/lib/db):

export default {
  // Replace your-framework with the framework you are using, e.g. react-vite, nextjs-vite, vue3-vite, etc.
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  viteFinal: async (config) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve?.alias,
        // 👇 External module
        lodash: import.meta.resolve('./lodash.mock'),
        // 👇 Internal modules
        '@/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;
  },
};

对应的 TypeScript 版本使用 @storybook/your-framework 导出的 StorybookConfig 类型做约束,其余逻辑一致:

// 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)'],
  viteFinal: async (config) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve?.alias,
        // 👇 External module
        lodash: import.meta.resolve('./lodash.mock'),
        // 👇 Internal modules
        '@/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;

几个需要重点理解的细节:

  • if (config.resolve) 守卫config 是 Vite 的 InlineConfig,某些边缘场景下 resolve 字段可能缺失。做展开前先判空,避免在 undefined 上解构抛错,是官方推荐写法的稳健性所在。
  • import.meta.resolve(...) 的作用:别名替换值必须是绝对路径/文件 URLimport.meta.resolve('./lodash.mock') 是 Node.js 原生 ESM 方法,能够把相对于当前 main.js(即 .storybook/ 目录)的路径解析成绝对文件 URL。mock 文件就放在 .storybook/ 目录下(如 .storybook/lodash.mock.storybook/api.mock.ts),与配置同目录便于定位。若你的运行环境不支持该方法,也可以改用 path.resolve(__dirname, '...') 等绝对路径方案。
  • Vite 的 alias 是前缀匹配@/lib/session 会命中所有以该字符串开头的导入。正因为如此,Vite 版本的 key 不需要(也不建议)额外加尾缀;但 Webpack 情况不同,见下文。

Webpack 构建器的别名配置与 $ 精确匹配

Webpack 的配置入口换成了 webpackFinal,结构完全对称:

export default {
  // Replace your-framework with the framework you are using (e.g., nextjs, react-webpack5)
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  webpackFinal: async (config) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve.alias,
        // 👇 External module
        lodash: import.meta.resolve('./lodash.mock'),
        // 👇 Internal modules
        '@/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;
  },
};
// Replace your-framework with the framework you are using (e.g., nextjs, react-webpack5)
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) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve.alias,
        // 👇 External module
        lodash: import.meta.resolve('./lodash.mock'),
        // 👇 Internal modules
        '@/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;

Vite 与 Webpack 的最大差异在于别名 key 的写法

  • Webpack 的 alias 默认是前缀匹配@/api 会同时吞掉 @/api@/api/utils 等所有子路径导入;
  • 若只想让 @/api 这一个确切导入被替换(这正是 mock 场景的诉求),必须在 key 末尾加 $(如 @/api$),表示精确匹配;
  • Vite 的别名语义并不依赖 $ 尾缀,因此两套配置的 key 必须分开维护:Vite 写 @/api,Webpack 写 @/api$

外部模块 lodash 在两种构建器下都不带 $,这是因为它的目标通常是「整个包」整体替换(例如用 ./lodash.mock 这个单文件 mock 顶替整个 lodash),前缀匹配反而符合预期。而项目内部模块常常需要保留子路径导入,比如 @/lib/db/users 应该继续按原逻辑解析,所以用 $ 锁死到 mock 那个入口本身。

实验性 CSF Next 配置风格:defineMain

如果项目启用了 Storybook 实验性的 CSF Next 配置(以 🧪 标识),.storybook/main.* 的写法从「默认导出对象」变为「调用 defineMain()」,并需要从对应框架包的 /node 子路径导入该函数。例如 Vite(React)下的 CSF Next 写法:

// Replace your-framework with the framework you are using (e.g., react-vite, 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)'],
  viteFinal: async (config) => {
    if (config.resolve) {
      config.resolve.alias = {
        ...config.resolve?.alias,
        // 👇 External module
        lodash: import.meta.resolve('./lodash.mock'),
        // 👇 Internal modules
        '@/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;
  },
});

defineMain 会对配置对象做类型推导与统一校验,使 frameworkstoriesviteFinal / webpackFinal 的字段类型更严格。框架包与构建器的对应关系需要注意:

  • Vue 3(Vite):从 @storybook/vue3-vite/node 导入 defineMainframework: '@storybook/vue3-vite'
  • Web Components(Vite):从 @storybook/web-components-vite/node 导入,framework: '@storybook/web-components-vite'
  • Angular(Webpack):从 @storybook/angular/node 导入,framework: '@storybook/angular',别名 key 同样需要 $ 精确匹配写法;
  • 其他框架(React、Next.js 等)统一使用 @storybook/<框架>/node 的形式。

这些入口文件就由仓库中各框架包的 package.jsonexports 映射导出,例如 code/frameworks/vue3-vitecode/frameworks/angularcode/frameworks/web-components-vite 等目录各自提供 /node 的 Node 侧入口。从源码结构看,defineMain 是 CSF Next 下对普通对象配置的类型安全包装,本质上仍是产出同样结构的 main 配置,只是享受了编译期校验。

在 stories 中消费被别名替换的模块

配置完成之后,组件与 story 里的 import 语句不需要任何改动,这也是别名方案最省心的地方:构建器在解析时已经把导入静默替换。若你需要在 story 内显式拿到 mock 对象以便设置返回值、做断言,则用与项目代码一致的别名导入即可,例如:

import { getUserFromSession } from '@/lib/session';

由于 mock 文件内部通过 fn(来自 storybook/test)包装了原函数,你得到的是完整的 Vitest mock 函数,可配合 mockReturnValue(value)mockResolvedValue(value)mockImplementation(fn) 等方法控制行为,或用 mockName() 保证压缩后名称可读。mock 文件只要被别名命中,其导出的 mock 函数就会在渲染阶段进入组件;而在 beforeEach、play function 等 hook 中动态调整返回值即可实现对不同场景的覆盖。

常见注意事项

把别名方案应用到真实项目时,有几个从上述配置结构与构建器语义中可以推导出的要点:

  1. mock 文件遵循「相对导入 + 全量再导出」约定:mock 文件内应使用相对路径导入原模块(绝不能再用别名导入自身),并通过 export * 透传原模块其余导出,只对需要 mock 的具名导出用 fn(actual.x) 覆盖;
  2. 区分「整个外部包」与「内部模块」两种映射:外部包走宽匹配,内部模块在 Webpack 下务必加 $;若某些工具库走深层导入(如 lodash-es/add),需要在别名里配置到对应深度的 key,或对包提供完整 mock 文件;
  3. 保持别名 key 与项目自身的解析配置一致:示例中的 @ 前缀别名通常是项目已在 tsconfig.json、Vite/Webpack 原生配置里定义好的;若项目还没有 @ 别名,可参考 storybook-main-ts-module-resolution-atsign-import.md 先在 Storybook 配置里把 @ 指向 src 目录;
  4. CSF 3 与 CSF Next 两套风格要选一套贯彻到底:普通导出对象用于 CSF 3,defineMain 用于实验性 CSF Next,混用会导致配置含义不一致;
  5. 优先考虑 Automocking:对于使用 Vite/Webpack 且未被其他测试工具占用 __mocks__ 约定的项目,官方文档建议优先使用 sb.mock 自动 mock;只有项目无法使用自动 mock(例如 Webpack 下需要 mock 带 CommonJS 入口的第三方包、或与其他工具的 mock 配置冲突)时,才退回到本文的别名方案。所有 mock 手段的取舍与边界条件,都汇总在 mocking-modules.mdx 中,可作为对照参考。

借助 viteFinal / webpackFinal 这两个构建器 hook,你可以在不侵入组件源码的前提下,把任何模块依赖在 Storybook 预览环境下无缝替换为可控的 mock 文件——这正是隔离式开发与组件级测试所依赖的基础设施。

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

项目优选

收起
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