首页
/ Storybook 中生成 MSW Service Worker:为组件 Stories 接入网络请求 Mock 的完整配置指南

Storybook 中生成 MSW Service Worker:为组件 Stories 接入网络请求 Mock 的完整配置指南

2026-09-07 10:18:49作者:秋泉律Samson

本指南聚焦 Storybook 文档中"Mocking network requests(模拟网络请求)"一节的核心前置步骤——通过 msw init 生成 Mock Service Worker(MSW)所需的 Service Worker 文件,并结合 staticDirs、项目级 loader 与 beforeEach({ msw }) 完成 REST/GraphQL 请求的 Story 级拦截。读完本文,你将能在自己的 Storybook 工作区中一键产出可用的 mockServiceWorker.js,并让 MSW 插件接管组件运行期间发出的真实网络请求。相关配套文档见 mocking-network-requests.mdx,本次讲解的命令原文出自 msw-generate-service-worker.md

为什么 Stories 需要"先生成一个 Service Worker"

MSW 是一套基于 Service Worker 的 API 拦截方案:它在浏览器预览环境中注册一个 Service Worker 脚本来捕获 fetch/XHR 请求,并返回由请求处理器(handler)准备的假数据。对于会发起真实网络请求的组件(例如从 REST 或 GraphQL API 拉取数据的页面),Storybook 里渲染这些组件时既不应该真的打到后端,也不应该让故事因网络不可用而失败,因此官方文档建议使用 MSW 插件(msw-storybook-addon)把 Mock 能力带进 Stories。

而"生成 Service Worker 文件"正是这条链路的物质基础:只有在项目静态目录中先放置 MSW 运行时脚本,浏览器才有可注册的 Worker 去接管后续请求。该文件并非手写,而是由 MSW 官方 CLI 的命令 msw init <目录> 一次性产出。也就是说,生成 Worker 这一步不是可选项,而是使用 MSW 插件在 Storybook 中拦截请求的前置条件

完整接入流程总览

围绕本主题,官方文档给出了一个可复制的四步流水线:

  1. 安装 mswmsw-storybook-addon(安装命令见 msw-addon-install.md,以 devDependency 形式写入);
  2. 运行 msw init 生成 Service Worker 文件(本文核心,命令见 msw-generate-service-worker.md);
  3. 在 Storybook 配置中通过 staticDirs 把 Worker 所在目录设为静态资源目录(配置示例见 main-config-static-dirs.md);
  4. 初始化插件并将其注册到所有 Stories:CSF 3 使用项目级 loaders: [mswLoader()],CSF Next 则在 definePreview 中通过 addons: [addonMsw()] 注册(示例见 msw-addon-initialize.md)。

后续在编写 Story 时,再通过 beforeEach({ msw }) 注入当前 Story 专属的请求处理器。下面按此顺序逐层展开,重点是第 2、3 步与命令背后的参数含义。

一条命令生成 Service Worker:三种包管理器的等价写法

原文档针对不同包管理器给出了三份等价命令,正文均为:

  • npm:npx msw init ./public --save
  • yarn:yarn dlx msw init ./public --save
  • pnpm:pnpm dlx msw init ./public --save

三者通过不同的包管理器临时执行 msw CLI,效果一致,可根据团队实际使用的包管理器任选其一执行。命令逐段拆解如下:

参数 含义 说明
init MSW CLI 的子命令 用于在指定目录中初始化/生成 Service Worker 脚本
./public Worker 文件的输出目录 默认放在项目的 public 目录,最终产出 ./public/mockServiceWorker.js
--save 记录配置 让 CLI 记住 Worker 输出目录,便于后续维护时无需重复指定完整参数

执行成功后会生成 mockServiceWorker.js——一个体积小、自包含的浏览器端运行时脚本,负责在页面与网络之间充当拦截代理。注意:该文件不应手工改动,它需要与所安装的 msw 版本保持严格一致,当升级 msw 依赖后通常需要重新执行一次生成命令以更新 Worker 脚本。

Angular 项目的目录差异(官方特别提醒)

官方文档在原命令后附加了一条框架相关的提醒:Angular 项目很可能需要调整目录参数,把 Worker 保存到不同于默认值的位置,例如保存到 src 目录。原因在于 Angular 项目的构建与静态资源组织方式与其他前端脚手架不同,Worker 必须放在能被浏览器按预期 URL 访问到的位置。因此 Angular 用户应把命令改写为类似 npx msw init ./src --save 的形式,并同步调整下一步 staticDirs 的指向。

用 staticDirs 把 Worker 暴露给 Storybook 预览

仅生成文件还不够。Storybook 的预览运行在一个 iframe 环境中,浏览器要能请求到 mockServiceWorker.js,就必须让该文件所在的目录成为 Storybook 可服务的静态目录。这正是 staticDirs 的作用。

配置位于 .storybook/main.js(或对应的 main.ts)中。官方给出的通用配置示例(main-config-static-dirs.md)默认已将 ../public 纳入静态目录:

export default {
  framework: '@storybook/your-framework',
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  staticDirs: ['../public', '../static'],
};

若你生成的 Worker 在默认的 public 目录(默认项目脚手架恰好把它配置为静态目录),则无需改动;但若像 Angular 那样把 Worker 放到了 src,就必须把对应目录补进 staticDirs,并保证这里的相对路径从 .storybook/ 出发能正确解析到 Worker 文件,否则插件在注册阶段将无法拉取到 Worker 脚本。

初始化插件:让所有 Stories 都能访问 Mock 上下文

Worker 就位后,需要让 Storybook 在渲染每个 Story 前启动 MSW 拦截环境。官方文档给出了两种写法(见 msw-addon-initialize.md)。

CSF 3 时代,在 .storybook/preview.js(或 preview.ts)中注册项目级 loader

import { mswLoader } from 'msw-storybook-addon/csf3';

export default {
  loaders: [mswLoader()],
};

使用 CSF Next(实验性)时,则在 definePreview 中把插件声明为 addon:

import { definePreview } from '@storybook/your-framework';

import addonMsw from 'msw-storybook-addon';

export default definePreview({
  addons: [addonMsw()],
});

loader 被注册在项目级意味着对项目中所有 Stories 生效:每个 Story 渲染前都会准备好 msw 上下文,供我们在 Story 定义中使用(见下节)。这是把网络 Mock 无缝接入每个组件故事的"总开关",缺少该步骤则后续 beforeEach({ msw }) 拿不到可用实例。

在 Stories 中按需拦截 REST 与 GraphQL 请求

完成上述基础设施后,即可编写真正"拦截网络请求"的 Stories。官方文档以 DocumentScreen 为例给出成功与失败两条 Story,本文摘录其 REST 版本核心形态(完整示例见 msw-addon-configure-handlers-http.md):

import { http, HttpResponse, delay } from 'msw';

import type { Meta, StoryObj } from '@storybook/your-framework';

import { DocumentScreen } from './YourPage';

const meta = {
  component: DocumentScreen,
} satisfies Meta<typeof DocumentScreen>;

export default meta;
type Story = StoryObj<typeof meta>;

export const MockedSuccess: Story = {
  beforeEach({ msw }) {
    msw.use(
      http.get('https://your-restful-endpoint/', () => {
        return HttpResponse.json(TestData);
      }),
    );
  },
};

export const MockedError: Story = {
  beforeEach({ msw }) {
    msw.use(
      http.get('https://your-restful-endpoint', async () => {
        await delay(800);
        return new HttpResponse(null, { status: 403 });
      }),
    );
  },
};

要点解读:

  • beforeEach({ msw }) 在每个 Story 渲染前执行,msw.use(...) 用于追加或覆盖当前请求处理器;
  • http.get 来自 msw 核心库,HttpResponse.json 用于快速返回 JSON Mock 数据;delay(800) 可模拟真实网络延迟,配合 new HttpResponse(null, { status: 403 }) 即可构造"加载中—失败"的交互时序;
  • 同一份数据在"成功"与"失败"两个 Story 间切换,就足以驱动组件把加载态、成功态、错误态三种 UI 完整展示出来,这正是组件测试与文档展示中最常用的手法。

GraphQL 场景思路一致,只是把 http.get 换成 graphql.query('AllInfoQuery', ...),并以 { data: { ... } }{ errors: [...] } 结构返回响应;官方同样提供了"查询成功"与"访问被拒"两个 Story 的对照写法(见 msw-addon-configure-handlers-graphql.md),并配套展示了 React(Apollo Client)、Svelte(URQL)、Vue3、Angular 等不同框架下如何为组件提供 Mock 客户端包裹层。

处理器的作用域:Story 级 / 组件级 / 项目级

从官方示例可以观察到一个重要的可伸缩规律:beforeEach 不只可以写在单个 Story 上,还可以提升到不同层级:

  • Story 级:仅在某个 Story 渲染前注册,适合"成功/失败"这类单点对照;
  • 组件(meta)级:把 beforeEach 写在组件导出的 meta 上,该文件内的所有 Stories 共享同一组处理器;
  • 项目级:把注册动作放进 preview.js/ts,对整个项目的全部 Stories 生效,适合全局性的默认 Mock。

若使用 CSF 3,还可以通过 msw 参数在 Story/组件/项目三个维度声明处理器,官方文档建议参考该页面对应旧版本的内容了解 msw 参数的具体写法。层级化的注册方式让 Mock 逻辑能够跟随组件规模自然演进,避免在大型项目中重复堆叠处理器。

版本注意事项:面向 MSW addon v3

官方文档特别指出,上文涉及的命令与代码片段面向 MSW 插件 v3。若项目仍在使用 v2,其接入方式与 API 有差异,需要查阅历史版本页面进行对照。升级到 v3 后,可使用社区提供的 codemod 一键迁移配置与 Stories:

npx msw-storybook-migrate

该迁移命令的作用是把 v2 时代的旧式写法自动改写为 v3 语法(如 msw 参数迁移为 beforeEach({ msw }) 风格),降低升级摩擦。从仓库证据看,Storybook 官方文档树以该命令配合 Callout 警告框的方式明确了 v2/v3 的行为边界(见 mocking-network-requests.mdx)。

验证与常见排错路径

完成上述配置后,可通过以下清单确认链路是否打通:

  1. 文件存在性:确认生成命令在预期目录产出了 mockServiceWorker.js(默认 public/ 下,Angular 通常在 src/ 下),且未手工改动内容;
  2. 版本一致性:升级 msw 依赖后重新执行一次生成命令,避免 Worker 脚本与库版本错配导致静默失效;
  3. 静态目录:核对 .storybook/main.jsstaticDirs 确实包含了 Worker 所在目录,且路径从 .storybook/ 出发可正确解析;
  4. 全局注册:确认 .storybook/preview.js 里已有 loaders: [mswLoader()](CSF 3)或 definePreview 中注册了 addonMsw()(CSF Next),否则 Story 中的 msw 上下文不可用;
  5. 作用域正确:确认目标 beforeEach 挂在正确的层级(Story/meta/project),避免出现"处理器注册了却未生效"的错觉。

至此,从生成 Service Worker 到 Story 级 REST/GraphQL 拦截的完整闭环已经建立:msw init 提供拦截运行时,staticDirs 保证它可被访问,mswLoader/addonMsw 把 Mock 能力注入每个 Story,而 beforeEach({ msw }) 则让你为不同故事精准编排各自的成功、失败与延迟场景。

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