首页
/ Storybook 中 Mock REST 请求:在 Story 内使用 MSW addon 配置 HTTP 处理器

Storybook 中 Mock REST 请求:在 Story 内使用 MSW addon 配置 HTTP 处理器

2026-09-07 15:27:19作者:劳婵绚Shirley

在 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 并列,作为数据层隔离的重要手段。其核心思路是:组件渲染时通过 fetchHttpClient 等发出真实网络请求,而 MSW 借助 service worker 在浏览器层面拦截这些请求并返回预设数据,组件自身代码无需改动。配套文档 mocking-network-requests.mdx 明确指出,这一机制适用于任何发起请求的库——只要该库基于标准的浏览器请求通道,fetchaxios、Angular HttpClient 等皆可被拦截。

需要特别留意:本系列指令与代码片段针对的是 msw-storybook-addon v3。若你正使用 v2,仓库文档建议先升级,并且 v3 提供了自动迁移命令:

npx msw-storybook-migrate

前置条件:addon 初始化与全局 MSW 环境

在单个 Story 内配置 REST 处理器之前,需要先完成三步全局准备(对应片段见 msw-addon-install.mdmsw-generate-service-worker.mdmsw-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

// .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 })mswundefined,请优先检查 .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 的视觉回归或组件测试能力进行验证。

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