Storybook 中生成 MSW Service Worker:为组件 Stories 接入网络请求 Mock 的完整配置指南
本指南聚焦 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 中拦截请求的前置条件。
完整接入流程总览
围绕本主题,官方文档给出了一个可复制的四步流水线:
- 安装
msw与msw-storybook-addon(安装命令见 msw-addon-install.md,以 devDependency 形式写入); - 运行
msw init生成 Service Worker 文件(本文核心,命令见 msw-generate-service-worker.md); - 在 Storybook 配置中通过
staticDirs把 Worker 所在目录设为静态资源目录(配置示例见 main-config-static-dirs.md); - 初始化插件并将其注册到所有 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)。
验证与常见排错路径
完成上述配置后,可通过以下清单确认链路是否打通:
- 文件存在性:确认生成命令在预期目录产出了
mockServiceWorker.js(默认public/下,Angular 通常在src/下),且未手工改动内容; - 版本一致性:升级
msw依赖后重新执行一次生成命令,避免 Worker 脚本与库版本错配导致静默失效; - 静态目录:核对
.storybook/main.js中staticDirs确实包含了 Worker 所在目录,且路径从.storybook/出发可正确解析; - 全局注册:确认
.storybook/preview.js里已有loaders: [mswLoader()](CSF 3)或definePreview中注册了addonMsw()(CSF Next),否则 Story 中的msw上下文不可用; - 作用域正确:确认目标
beforeEach挂在正确的层级(Story/meta/project),避免出现"处理器注册了却未生效"的错觉。
至此,从生成 Service Worker 到 Story 级 REST/GraphQL 拦截的完整闭环已经建立:msw init 提供拦截运行时,staticDirs 保证它可被访问,mswLoader/addonMsw 把 Mock 能力注入每个 Story,而 beforeEach({ msw }) 则让你为不同故事精准编排各自的成功、失败与延迟场景。
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 StartedRust0625
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