Skip to content

3.1 setupWorker / setupServer 与三大命名空间 ​

MSW 的 API 表面非常小:启动器 + 协议命名空间 + 响应构造器。本章给你一张完整地图。

启动器 ​

ts
// 浏览器
import { setupWorker } from "msw/browser";
const worker = setupWorker(...handlers);
await worker.start();

// Node(Vitest / Jest / 任意脚本)
import { setupServer } from "msw/node";
const server = setupServer(...handlers);
server.listen();

两者接收同样的 handler 数组,返回同构的 API:use()(追加 handler)、resetHandlers()、restoreHandlers()、close()。 这正是"一套 mock,开发与测试共用"的技术基础。

混合 HTTP 与 WS handler 时,数组类型注解用 AnyHandler[]:

ts
import type { AnyHandler } from "msw";

export const handlers: AnyHandler[] = [
  http.get("*/api/users", () => HttpResponse.json([])),
  chatHandler,
];

http 命名空间 ​

http.get/post/put/patch/delete/head/options/all,签名统一:

ts
http.method(predicate, resolver)

// predicate:字符串路径 / Path-to-RegExp 模板 / 通配源
http.get("*/api/users/:id", ({ params, request, cookies }) => {
  const { id } = params;          // 路径参数
  const url = new URL(request.url);
  const q = url.searchParams.get("q"); // 查询参数
  const token = request.headers.get("authorization");
  return HttpResponse.json({ id, q, token });
});

resolver 接收的解析信息对象常用字段:

字段内容
request标准 Request,可读 headers / json() / formData() / text() / arrayBuffer()
params路径参数(如 /users/:id 的 id)
cookies解析后的 Cookie 对象
requestId本次请求唯一 id(写日志/链路追踪很有用)

http.all("*/api/*", resolver) 匹配该路径的所有方法,常用于统一鉴权守卫。

HttpResponse 响应构造器 ​

ts
// JSON(默认 200,自动带 Content-Type: application/json)
HttpResponse.json({ ok: true });

// 自定义状态码与响应头
HttpResponse.json({ error: { code: "NO_AUTH" } }, {
  status: 401,
  headers: { "WWW-Authenticate": "Bearer" },
});

// 201 Created + Location 头(RESTful 最佳实践)
HttpResponse.json(user, { status: 201, headers: { Location: `/api/users/${user.id}` } });

// 文本 / XML / 任意 body
new HttpResponse("pong", { headers: { "Content-Type": "text/plain" } });
new HttpResponse("<rss/>", { headers: { "Content-Type": "application/xml" } });

// 二进制(文件下载)
new HttpResponse(new Uint8Array(pngBytes), {
  headers: { "Content-Type": "image/png" },
});

// 网络层失败(断网、CORS 失败)——fetch 直接 reject
HttpResponse.error();

graphql 命名空间(msw/graphql) ​

ts
import { graphql } from "msw/graphql";

const gql = graphql.link("*/graphql"); // 单端点;link 固定源与路径

// 按操作名匹配 query / mutation
export const listUsers = gql.query("ListUsers", ({ variables }) =>
  HttpResponse.json({ data: { users: db.users.paginate(variables) } }),
);

export const createUser = gql.mutation("CreateUser", async ({ request, variables }) => {
  if (!request.headers.get("authorization")) {
    // GraphQL 标准错误信封
    return HttpResponse.json({
      errors: [{ message: "未登录", extensions: { code: "UNAUTHENTICATED" } }],
    });
  }
  return HttpResponse.json({ data: { createUser: { user: create(variables.input) } } });
});

要点:

  • graphql.query("操作名", resolver) 按 operationName 分发,与 HTTP 方法无关;
  • resolver 必须内联 HttpResponse.json(...)(辅助函数返回类型会宽化,导致类型错误);
  • 查询参数 / 变更变量从 variables 取,请求体仍可从 request.json() 拿;
  • 错误用标准 { errors: [{ message, extensions: { code } }] } 信封。

ws 命名空间(msw/ws) ​

ts
import { ws } from "msw/ws";

const chat = ws.link("ws://*/ws/chat");

export const chatHandler = chat.addEventListener("connection", ({ client, server }) => {
  // client:浏览器侧的连接;server:handler 侧用于主动推消息
  server.connect();
  client.send(JSON.stringify({ type: "system", text: "欢迎来到信标驿站" }));

  client.addEventListener("message", (event) => {
    // 广播给所有在线 client;排除发送者用 chat.broadcastExcept(client, payload)
    chat.broadcast(String(event.data));
  });
});

下一章会专门讲 Worker 生命周期,之后进入拦截与响应的细节。先用下面的控制台热热身: 第一个请求观察默认 300ms 延迟,第二个直接命中 503:

http.get 体验

下载示例文件 ​