news 2026/9/18 5:32:53

Storybook Automocking 系列教程:在 .storybook/preview.* 中用 `sb.mock` 注册 Mock 文件(register mock file)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Storybook Automocking 系列教程:在 .storybook/preview.* 中用 `sb.mock` 注册 Mock 文件(register mock file)

Storybook Automocking 系列教程:在 .storybook/preview.* 中用sb.mock注册 Mock 文件(register mock file)

本篇技术指南聚焦 Storybook 自动模拟(Automocking)三种注册方式中的Mock 文件(Mock files)模式:如何在项目级配置.storybook/preview.*中通过storybook/testsb.mock工具注册本地模块与node_modules包的自定义 mock 文件,从而实现复杂、可复用、可跨 story 共享的模块替换。读完本文将掌握 mock 文件目录布局、sb.mock各语法形态(CSF 3 与 CSF Next、TS 与 JS)、路径解析规则,以及底层源码的运行原理。

背景:Automocking 的两种能力来源

Storybook 在storybook/test中提供sb.mock()工具注册需要模拟的模块。同一套 API 支持两种替换来源:

  1. 自动生成 mock/spy:当未找到对应 mock 文件时,Storybook 在构建期自动把原模块的导出替换为 Vitest mock 函数;
  2. Mock 文件(本文主题):开发者预先编写一个真实的替换文件(通常放在__mocks__目录),sb.mock会优先查找并使用该文件。

正如 mocking-modules.mdx 中的说明:注册自动 mock 与注册 mock 文件的 API 完全一致,唯一区别是sb.mock会先在对应目录查找是否存在 mock 文件(redirect),找不到才回退到自动 mock。

为什么需要 mock 文件?当模拟逻辑较复杂、或需要在多个 story 间复用一个 mock 行为时,把模拟实现写进独立文件更清晰;同时 mock 文件能彻底阻断原模块代码执行——这一点与"完全自动 mock"不同,后者虽然会替换导出函数,但模块自身及其依赖仍会被求值(相关说明)。

第一步:按规则创建 Mock 文件

本地模块的 mock 文件

针对项目内的本地模块,在与模块同级的__mocks__目录下创建同名文件。例如要模拟lib目录下的session模块:

lib/ ├── session.ts └── __mocks__/ └── session.ts
export function getUserFromSession() { return { name: 'Mocked User' }; }

node_modules 包的 mock 文件

针对外部依赖包,在项目根目录__mocks__目录创建 mock 文件。例如模拟uuid包:

__mocks__/ └── uuid.js
export function v4() { return '1234-5678-90ab-cdef'; }

若外部模块带深层导入路径(如lodash-es/add),需要按路径层级建目录,例如__mocks__/lodash-es/add.js

"项目根目录"随构建器不同而不同

构建器__mocks__目录位置
ViteVite 配置中的root目录(通常为process.cwd());若无法解析则回退到包含.storybook的目录
WebpackWebpack 配置中的context目录(通常为process.cwd());若无法解析则回退到仓库根目录

Mock 文件硬性要求

  • 必须用JavaScript编写(不能用 TypeScript),且使用ESModules(不能用 CJS);
  • 必须与原模块导出同名的命名导出(named exports);如需模拟默认导出,可在 mock 文件中使用export default

第二步:在.storybook/preview.*注册 Mock 文件

注册 mock 文件的调用放在项目级配置.storybook/preview.tsx(或.jsx/.ts/.js)中,这正是本文目标代码片段 automock-register-mock-file.md 的核心内容。它同时替换两处依赖:本地模块../lib/session.ts与外部包uuid

CSF 3 形态(TypeScript)

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, vue3-vite, sveltekit) import type { Preview } from '@storybook/your-framework'; import { sb } from 'storybook/test'; // 👇 Replaces imports of this module with imports to `../lib/__mocks__/session.ts` sb.mock(import('../lib/session.ts')); // 👇 Replaces imports of this module with imports to `../__mocks__/uuid.ts` sb.mock(import('uuid')); const preview: Preview = { // ... }; export default preview;

CSF 3 形态(JavaScript)

import { sb } from 'storybook/test'; // 👇 Replaces imports of this module with imports to `../lib/__mocks__/session.ts` sb.mock('../lib/session.js'); // 👇 Replaces imports of this module with imports to `../__mocks__/uuid.ts` sb.mock('uuid'); export default { // ... };

CSF Next 形态(definePreview

使用新实验性 CSF Next 语法时,改为从框架包导入definePreviewsb.mock调用体不变。以 React 与 Vue 为例:

// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) import { definePreview } from '@storybook/your-framework'; import { sb } from 'storybook/test'; // 👇 Replaces imports of this module with imports to `../lib/__mocks__/session.ts` sb.mock(import('../lib/session.ts')); // 👇 Replaces imports of this module with imports to `../__mocks__/uuid.ts` sb.mock(import('uuid')); export default definePreview({ // ... });
import { definePreview } from '@storybook/vue3-vite'; import { sb } from 'storybook/test'; sb.mock(import('../lib/session.ts')); sb.mock(import('uuid')); export default definePreview({ // ... });

CSF Next 形态下definePreview的导入源随渲染器切换,代码骨架完全相同:

渲染器definePreview 导入源
React@storybook/your-framework(react-vite、nextjs、nextjs-vite 等)
Vue@storybook/vue3-vite
Angular@storybook/angular
Web Components@storybook/web-components-vite

两行注释意味着什么

上面示例里的两行注释揭示了一条关键事实:mock 文件路径并非由开发者显式写出sb.mock(import('../lib/session.ts'))指向的是待模拟的原模块,系统会自动到其相邻的__mocks__目录查找替换文件;sb.mock(import('uuid'))则会到项目根__mocks__下查找uuid.js。这就是文档所称"API 相同、仅查找顺序不同"的实现形态。

sb.mock注册的核心约束(务必遵守)

围绕注册 mock 文件与自动 mock,sb.mock的规则是相同的(mocking-modules.mdx):

  1. 注册位置:只能在项目级配置.storybook/preview.*中注册。story 文件中不能调用sb.mock注册新模块,只能通过beforeEach/play等运行时修改已注册 mock 的行为;
  2. 可注册对象:本地模块(如../lib/session.ts)与node_modules包(如uuid)均可;
  3. 本地模块的路径要求
    • 不得使用别名或 subpath(如@/lib/session.ts#lib/session);
    • 必须相对于.storybook/preview.*文件本身;
    • 必须包含文件扩展名(如.ts.js);
  4. TypeScript 推荐写法:用import()包裹模块路径,如sb.mock(import('../lib/session.ts')),以保证模块能被正确解析与获得类型;
  5. Webpack 用户的额外限制:Webpack 构建器只能自动 mock 拥有纯 ESM 入口的node_modules包。若包同时提供 CJS 与 ESM 入口,Webpack 无法正确解析 ESM 入口,此时应改用 mock 文件方式(automock-register-mock-file的场景正是解决此问题的推荐路径)。若强行走自动 mock,常见报错为exports is not defined

在 Story 中控制 Mock 文件的行为并断言

mock 文件导出的函数会被注册为完整的 Vitest mock 函数,因此可在 story 的beforeEach(在渲染前执行)或play中设置返回值并断言调用。注意此时应使用storybook/testmocked工具获取正确的 TS 类型(它是vi.mocked的类型安全封装):

import type { Meta, StoryObj } from '@storybook/your-framework'; import { expect, mocked } from 'storybook/test'; import { AuthButton } from './AuthButton'; import { v4 as uuidv4 } from 'uuid'; import { getUserFromSession } from '../lib/session'; const meta = { component: AuthButton, // 👇 Runs before each story renders beforeEach: async () => { // 👇 Force known, consistent behavior for mocked modules mocked(uuidv4).mockReturnValue('1234-5678-90ab-cdef'); mocked(getUserFromSession).mockReturnValue({ name: 'John Doe' }); }, } satisfies Meta<typeof AuthButton>; export default meta; type Story = StoryObj<typeof meta>; export const LogIn: Story = { play: async ({ canvas, userEvent }) => { const button = canvas.getByRole('button', { name: 'Sign in' }); userEvent.click(button); // Assert that the getUserFromSession function was called expect(getUserFromSession).toHaveBeenCalled(); }, };

由于 mock 文件提供的是完整 Vitest mock 函数,最常用的方法包括:

方法用途
mockReturnValue(value)设定同步返回值
mockResolvedValue(value)设定异步函数 resolve 的值
mockImplementation(fn)设定自定义实现

无需手工清理这些 mock:Storybook 在渲染每条 story 前会自动恢复 mock(对应parameters.test.restoreMocks行为)。

源码级原理:sb.mock如何找到并注入 Mock 文件

从本仓库源码可以进一步印证注册机制的两段式处理。

阶段一:解析与重定向(resolve)

在 code/core/src/mocking-utils/resolve.ts 中,resolveMock()(resolve.ts#L54-L79)先判断模块是否为外部包:

  • 外部包通过resolveExternalModule()解析——使用oxc-resolver,条件为browser/import/module/default,优先命中exports映射与package.jsonbrowser字段(resolve.ts#L15-L36);
  • 本地模块则用require.resolve(path, { paths: [dirname(importer)] })preview 文件所在目录为基准解析(印证了"相对.storybook/preview.*"的约束);
  • 随后调用findMockRedirect在对应__mocks__目录查找替换文件,得到redirectPath;找不到时为null,从而回退到自动 mock。

阶段二:构建期代码变换(automock / autospy)

当没有 mock 文件时,构建器会改写原模块导出。在 code/core/src/mocking-utils/automock.ts 中,getAutomockCode()(automock.ts#L17-L22)调用基于 MagicString 的automockModule()

  • 解析原模块 AST,收集所有命名导出(函数、变量、类、重导出与默认导出);
  • 通过globalThis['__vitest_mocker__']访问 Vitest mock 注册表,调用mockObject(module, 'automock' | 'autospy')生成替换后的模块对象;
  • autospy对应{ spy: true }(保留原行为),automock对应默认的完全替换;
  • 生成的新模块再以export { ... }重新声明。因此所有替换决策在构建期静态完成,产物直接内联真正的 mock 模块,无运行时拦截开销。

这套变换逻辑被 vite-mock 插件(扫描.storybook/preview.*中的sb.mock()调用)与 Webpack 侧 webpack-automock-loader 复用。开发模式下,mock 文件的新增/变更还会通过 Vite 模块图失效机制触发热更新。

与 Vitest mocking 的差异

仓库文档明确强调其与 Vitest 原生 mocking 的差异(mocking-modules.mdx):mock 全局化且仅限.storybook/preview.*;决策静态化于构建期,因此没有sb.unmock(),也不接受工厂函数(sb.mock('path', () => ({...})))——工厂函数是运行时行为,与构建期静态替换矛盾。mock 文件的函数行为仍可在 story 的play/beforeEach中运行时修改。

仓库内的自动化验证

本仓库自带的示例与测试可以佐证上述全部机制:

  • ModuleAutoMocking.stories.ts 与其mocks目录下的工具 mock,演示本地模块注册 mock 文件后的 story 编写方式;
  • NodeModuleMocking.stories.js 演示node_modules包级 mock;
  • 端到端层面 sb-module-mocking.spec.ts 覆盖了模块模拟的完整流程。

小结

Mock 文件是 Storybook Automocking 中最灵活的一种注册形态:本地模块把替换文件放在模块相邻的__mocks__目录,外部包把替换文件放在项目根__mocks__目录;随后在.storybook/preview.*用一行sb.mock(import('../lib/session.ts'))sb.mock(import('uuid'))完成全局注册,mock 文件导出即成为可断言、可复用的 Vitest mock 函数。理解"先查 mock 文件、后自动 mock"的分流逻辑与构建期静态替换的机制,能帮助你在 Vite/Webpack 项目中稳定地隔离组件的外部依赖。

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

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

Ubuntu安装包备份与恢复方案:apt缓存、dpkg-repack与第三方deb归档

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 5:27:42

SpringBoot+Vue代驾系统架构设计与高并发优化实践

1. 企业级代驾管理系统架构解析代驾行业近年来呈现爆发式增长&#xff0c;传统的人工调度和纸质记录方式已无法满足现代出行需求。我们团队基于SpringBootVueMyBatis技术栈&#xff0c;开发了一套高可用代驾管理系统&#xff0c;经过半年实际运营验证&#xff0c;系统日均处理订…

作者头像 李华
网站建设 2026/9/18 5:27:14

STM32环境监测系统:工业级传感设计与工程落地实践

1. 这不是个“玩具项目”&#xff0c;而是一套可落地的环境监测工程实践STM32项目开源&#xff1a;环境质量监测系统&#xff08;代码原理图仿真&#xff09;——这行标题里藏着三个硬核关键词&#xff1a;STM32、环境质量监测、开源交付物。它不是实验室里亮几个LED的Demo&…

作者头像 李华
网站建设 2026/9/18 5:26:35

读 Nanobot 源码时 OpenClaw 反复 401?TaoToken 的 Base URL 落到 /api

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 5:25:49

Git 2.41.0 安装教程:多平台下载校验、配置与排错指南

1. 锁定 2.41.0&#xff1a;什么场景值得这么干&#xff0c;什么场景纯属折腾先把结论放在最前面。Git 2.41.0 安装教程这类内容之所以一直有人搜&#xff0c;核心原因并不是这个版本有什么石破天惊的新能力&#xff0c;而是"版本统一"这件事在真实项目里的权重远比大…

作者头像 李华