Storybook 中 Mock REST 请求:在 Story 内使用 MSW addon 配置 HTTP 处理器
在 Storybook 中开发依赖后端接口的组件(例如文档详情页需要从 REST API 拉取用户、文档与子文档数据)时,通常希望每个 Story 都能稳定地展示"数据加载成功""接口返回错误"等不同分支。本指南基于 Storybook 仓库中的 MSW(Mock Service Worker)addon 配置片段 msw-addon-configure-handlers-http.md,讲解如何在 Story 的 beforeEach 钩子中通过 msw.use(...) 注册 REST 请求处理器,覆盖 CSF 3 与 CSF Next 两种写法以及 Angular、React、Svelte、Vue、Web Components 等主流渲染器。读完本文,你将能够为任意组件 Story 精确模拟"成功返回 JSON"与"延迟后返回错误状态码"两类 HTTP 场景。
背景:REST 请求在 Story 中的隔离问题
Storybook 官方将"网络请求 Mock"与模块 Mock 并列,作为数据层隔离的重要手段。其核心思路是:组件渲染时通过 fetch、HttpClient 等发出真实网络请求,而 MSW 借助 service worker 在浏览器层面拦截这些请求并返回预设数据,组件自身代码无需改动。配套文档 mocking-network-requests.mdx 明确指出,这一机制适用于任何发起请求的库——只要该库基于标准的浏览器请求通道,fetch、axios、Angular HttpClient 等皆可被拦截。
需要特别留意:本系列指令与代码片段针对的是 msw-storybook-addon v3。若你正使用 v2,仓库文档建议先升级,并且 v3 提供了自动迁移命令:
npx msw-storybook-migrate
前置条件:addon 初始化与全局 MSW 环境
在单个 Story 内配置 REST 处理器之前,需要先完成三步全局准备(对应片段见 msw-addon-install.md、msw-generate-service-worker.md 与 msw-addon-initialize.md)。
第一步:安装依赖
# npm
npm install msw msw-storybook-addon --save-dev
# pnpm
pnpm add msw msw-storybook-addon --save-dev
# yarn
yarn add msw msw-storybook-addon --save-dev
第二步:生成 service worker 文件(若项目尚未使用 MSW):
# npm
npx msw init ./public --save
# yarn
yarn dlx msw init ./public --save
# pnpm
pnpm dlx msw init ./public --save
Angular 项目往往需要把 mock service worker 输出到不同目录(例如 src),此时应相应调整 init 的目标路径,并让 Storybook 的 staticDirs 配置指向该目录(Angular 框架下 Storybook 需能访问到生成的 mockServiceWorker.js)。
第三步:在全局 preview 中注册 MSW
- 使用 CSF 3 时,通过项目级 loaders 注册
mswLoader():
// .storybook/preview.ts(CSF 3)
import type { Preview } from '@storybook/your-framework';
import { mswLoader } from 'msw-storybook-addon/csf3';
const preview: Preview = {
// 为所有 Story 注册 MSW loader
loaders: [mswLoader()],
};
export default preview;
- 使用 CSF Next 时,将 addon 加入
definePreview({ addons: [...] }):
// .storybook/preview.ts(CSF Next)
import { definePreview } from '@storybook/your-framework';
import addonMsw from 'msw-storybook-addon';
export default definePreview({
addons: [addonMsw()],
});
只有完成上述注册,Story 的 beforeEach({ msw }) 回调中才会出现可用的 msw 对象。
待 Mock 的组件示例
本文假设存在一个文档屏组件 DocumentScreen,它在初始化时请求 https://your-restful-endpoint,根据响应渲染用户、主文档及子文档列表,并在请求失败时展示错误提示。React 版本大致如下(完整多框架版本见 document-screen-fetch.md):
// YourPage.jsx
import React, { useState, useEffect } from 'react';
function useFetchData() {
const [status, setStatus] = useState('idle');
const [data, setData] = useState([]);
useEffect(() => {
setStatus('loading');
fetch('https://your-restful-endpoint')
.then((res) => {
if (!res.ok) {
throw new Error(res.statusText);
}
return res.json();
})
.then((data) => setData(data))
.catch(() => setStatus('error'));
}, []);
return { status, data };
}
组件通过浏览器网络通道发请求,正是 MSW 能拦截的前提。
在 Story 中用 beforeEach({ msw }) 注册 REST 处理器
MSW addon v3 允许你在每个 Story 内部按需配置请求处理器。处理器定义在 Story 的 beforeEach 钩子里,钩子参数解构出 msw 对象,随后调用 msw.use(handler...) 将处理器装载到 mock server 上。
下面是仓库片段中最通用的一份完整 CSF 3 代码(Renderer 无关的 TS/JS 写法),它演示了一个文档屏的两种关键状态:
MockedSuccess:命中GET https://your-restful-endpoint/,用HttpResponse.json(TestData)返回模拟数据,组件进入"成功渲染"分支;MockedError:同样命中 GET 请求,但先await delay(800)模拟网络延迟,再返回status: 403的空响应,组件进入"错误提示"分支。
// YourPage.stories.ts(CSF 3,以 react-vite / nextjs 等为例)
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>;
// 👇 Story 中将使用的模拟数据
const TestData = {
user: {
userID: 1,
name: 'Someone',
},
document: {
id: 1,
userID: 1,
title: 'Something',
brief: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
status: 'approved',
},
subdocuments: [
{
id: 1,
userID: 1,
title: 'Something',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
status: 'approved',
},
],
};
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,
});
}),
);
},
};
关键 API 逐项拆解
http.get(url, resolver):MSW v2 提供的 REST 请求描述器,指定要拦截的方法与 URL。本片段中 MockedSuccess 的 URL 尾带/,MockedError 不带,二者在多数服务端路由下等价,但实践中请保持与你组件实际请求的 URL 完全一致,否则拦截不会命中。HttpResponse.json(TestData):以 JSON 形式返回响应体,等价于携带Content-Type: application/json的 200 响应。new HttpResponse(null, { status: 403 }):构造无响应体的自定义状态码响应,用于模拟鉴权失败、服务端错误等分支。delay(800):在 resolver 中异步等待后再返回,可模拟慢网络,配合 Storybook 可观察加载态(loading)UI。beforeEach({ msw }):addon v3 的钩子机制。每个渲染器都会为测试钩子注入一个 MSW 上下文对象;msw.use只在该 Story 渲染前生效,从而让不同 Story 拥有互不干扰的接口行为。
各渲染器 / 编写风格下的等效实现
原片段的价值在于为每种框架都给出了可直接复制的等效代码。各版本核心逻辑完全一致,差异仅在于:meta 的定义方式、Story 导出形态以及组件导入路径。下表汇总了该片段实际覆盖的变体,便于你对照自己的项目:
| 渲染器 / 风格 | 片段内文件命名 | Story 形态 |
|---|---|---|
| Angular(CSF 3) | YourPage.stories.ts |
export const MockedSuccess: Story = { beforeEach({ msw }) {...} } |
| Angular(CSF Next 🧪) | YourPage.stories.ts |
export const MockedSuccess = meta.story({ beforeEach({ msw }) {...} }) |
| Svelte(Svelte CSF) | YourPage.stories.svelte |
<Story name="MockedSuccess" beforeEach={({ msw }) => {...}} /> |
| Svelte(CSF 3,JS/TS) | YourPage.stories.js|ts |
常规 export const Story |
| React / 通用(CSF 3) | YourPage.stories.js|tsx |
常规 export const Story |
| React / Vue / Angular / Web Components(CSF Next 🧪) | YourPage.stories.ts |
meta.story({...}) |
| Vue(CSF Next 🧪) | YourPage.stories.js|ts |
meta.story({...}) |
| Web Components(CSF 3,JS/TS) | YourPage.stories.js|ts |
常规 export const Story(component: 'demo-document-screen') |
Angular(CSF 3)
Angular 的组件类型化写法依赖 StoryObj<DocumentScreen>,其中 DocumentScreen 直接 import 自 .component.ts 文件:
// YourPage.stories.ts(Angular,CSF 3)
import { http, HttpResponse, delay } from 'msw';
import type { Meta, StoryObj } from '@storybook/angular';
import { DocumentScreen } from './your-page.component';
const meta: Meta<DocumentScreen> = {
component: DocumentScreen,
};
export default meta;
type Story = StoryObj<DocumentScreen>;
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,
});
}),
);
},
};
其中 TestData 的结构(user / document / subdocuments)应与你组件 ngOnInit 中从响应读取的字段保持一致——文档屏组件在成功后会执行 this.user = data.user 等赋值。
Angular / React / Vue / Web Components(CSF Next 🧪)
CSF Next 实验性语法把 Story 定义为 meta.story(...),并把组件配置收敛到 preview.meta(...)。示例见下(Angular 版,其余渲染器结构相同,仅 import 来源不同):
// YourPage.stories.ts(Angular,CSF Next 🧪)
import { http, HttpResponse, delay } from 'msw';
import preview from '../.storybook/preview';
import { DocumentScreen } from './your-page.component';
const meta = preview.meta({
component: DocumentScreen,
});
export const MockedSuccess = meta.story({
beforeEach({ msw }) {
msw.use(
http.get('https://your-restful-endpoint/', () => {
return HttpResponse.json(TestData);
}),
);
},
});
export const MockedError = meta.story({
beforeEach({ msw }) {
msw.use(
http.get('https://your-restful-endpoint', async () => {
await delay(800);
return new HttpResponse(null, {
status: 403,
});
}),
);
},
});
Svelte CSF
Svelte 采用 <Story> 组件式声明,beforeEach 作为 prop 传入回调,msw.use 的用法不变。同时测试数据可放在 <script module> 中,供同文件多个 Story 共享:
<!-- YourPage.stories.svelte(Svelte CSF) -->
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import { http, HttpResponse, delay } from 'msw';
import DocumentScreen from './YourPage.svelte';
const { Story } = defineMeta({
component: DocumentScreen,
});
// 👇 Story 中将使用的模拟数据
const TestData = {
user: { userID: 1, name: 'Someone' },
document: { id: 1, userID: 1, title: 'Something', brief: 'Lorem ipsum...', status: 'approved' },
subdocuments: [ /* ... */ ],
};
</script>
<Story
name="MockedSuccess"
beforeEach={({ msw }) => {
msw.use(
http.get('https://your-restful-endpoint/', () => {
return HttpResponse.json(TestData);
}),
);
}}
/>
<Story
name="MockedError"
beforeEach={({ msw }) => {
msw.use(
http.get('https://your-restful-endpoint', async () => {
await delay(800);
return new HttpResponse(null, { status: 403 });
}),
);
}}
/>
Web Components(CSF 3)
Web Components 渲染器中 component 字段声明的是自定义元素标签名而非类,因此 meta 中写 component: 'demo-document-screen':
// YourPage.stories.ts(Web Components,CSF 3)
import type { Meta, StoryObj } from '@storybook/web-components-vite';
import { http, HttpResponse, delay } from 'msw';
const meta: Meta = {
component: 'demo-document-screen',
};
export default meta;
type Story = StoryObj;
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 });
}),
);
},
};
处理器作用域:Story / 组件 / 项目三级复用
从本片段的 Story 级 beforeEach 出发,同一套 msw.use 注册模式还可以上移到更大作用域(mocking-network-requests.mdx 中明确说明):
- Story 级:
beforeEach写在某个具名 Story 上,仅该 Story 生效——适用于本文演示的"成功/失败各一条"场景; - 组件(meta)级:
beforeEach写在meta/preview.meta(...)上,同一文件内的所有 Story 共享公共 handlers(如固定返回当前登录用户信息的接口); - 项目级:
beforeEach写在preview.ts/definePreview的全局配置中,全项目 Story 生效,例如一次性拦截埋点、配置接口。
此外,在 CSF 3 场景下,若你倾向旧式声明,也可使用 msw parameter 在 Story / 组件 / 项目三级定义 handlers;仓库文档提示这类写法属于 v3 之前的用法,新项目建议统一采用上述 beforeEach 体系。
为什么测试钩子里能拿到 msw?
beforeEach 回调中的 msw 上下文之所以可用,是因为第三步全局注册已经生效:CSF 3 下 mswLoader() 作为项目级 loader 被 Storybook 执行,把 MSW 的 mock server 上下文注入到每个 Story 的钩子参数中;CSF Next 下 addonMsw() 承担相同职责。因此排查问题时,若发现 beforeEach({ msw }) 中 msw 为 undefined,请优先检查 .storybook/preview 是否正确注册了 loader/addon,以及 service worker 文件路径是否与 staticDirs 一致。
延伸:GraphQL 与整页文档示例
REST 之外的 GraphQL 请求也可以用完全相同的作用域与钩子模型来 Mock,只是把 http.get 换成 MSW 的 graphql.query / graphql.mutation 描述器,完整代码见 msw-addon-configure-handlers-graphql.md;配合 document-screen-fetch.md(REST + fetch/HttpClient)与 document-screen-with-graphql.md(GraphQL + Apollo/URQL),你可以把"数据屏组件"的加载、成功、错误三种 UI 状态在 Storybook 中完整覆盖,再交由 addon 的视觉回归或组件测试能力进行验证。
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 StartedRust0627
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