Mock Service Worker 是一个 API 模拟库,它使用 Service Worker API 来拦截实际请求。
请求流程图
该库注册了一个 Service Worker,通过 fetch 事件监听应用程序的出站请求,将这些请求指向客户端库,并发送模拟响应(如果有)给工作线程以便进行响应。
浏览器端 Mocking REST API 使用流程:
- 安装
npm i msw -D
- 通过 Mock Service Worker 提供的 CLI 来注册 service worker
sh
npx msw init <PUBLIC_DIR> --save
// <PUBLIC_DIR> 为公共目录
// 如果是 Create React App 用 public/ 代替 <PUBLIC_DIR>
npx msw init public/ --save
- 定义请求处理程序
ts
// src/mocks/handler.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/test', async (req, res, ctx) => {
return res(ctx.delay(3000), ctx.status(200), ctx.json({ data: 'ok' }));
}),
];
- 用
setupWorker
生成一个 service worker 实例;
ts
// src/mocks/browser.ts
import { setupWorker } from 'msw';
import { handlers } from './handler';
const getHandler = () => {
/** 此处要使用项目中实际的 baseUrl */
const baseURL = 'http://127.0.0.1';
/** 处理,给每个路径加上 baseUrl */
handlers.forEach((item: any) => {
const fullPath = baseURL + item.info.path;
item.info.header = `${item.info.method} ${fullPath}`;
item.info.path = fullPath;
});
return handlers;
};
export const worker = setupWorker(...getHandler());
- 判断环境,在开发环境中激活 service worker(此时是全局拦截,任何 fetch 请求都会被拦截)
tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mocks/browser')
worker.start({
quiet: false, // 是否禁止在控制台打印匹配的日志记录
onUnhandledRequest: "bypass", // 对于没有 mock 的接口直接通过,原样执行
})
}
ReactDOM.render(<App />, document.getElementById('root'))
完成以上步骤,启动项目后控制台有以下提示,说明 MSW 启动成功:
功能特性
条件响应
当响应解析器返回 req.passthrough 调用时,MSW将按原样执行捕获的请求,命中实际端点并返回其响应。
ts
rest.get('/user', (req, res, ctx) => {
const userId = req.url.searchParams.get('userId')
if (userId === 'abc-123') {
return res(
ctx.json({
firstName: 'John',
lastName: 'Maverick',
}),
)
}
/** 返回 req.passthrough,请求会发送到服务器 */
return req.passthrough()
})
响应补丁
响应补丁是一种技术,当模拟的响应基于实际响应时使用。这种技术在使用现有API并打算为各种目的(例如实验或调试)增强它时可能非常有用。
ts
rest.get('https://api.github.com/users/:username', async (req, res, ctx) => {
/** 发送请求,拿到真实数据,可以在该数据上基础上进行修改再响应 */
const originalResponse = await ctx.fetch(req)
const originalResponseData = await originalResponse.json()
return res(
ctx.json({
location: originalResponseData.location,
firstName: 'Not the real first name',
}),
)
}),
将拦截的请求对象传递给 ctx.fetch() 调用以获取原始响应。使用 ctx.fetch() 执行一个绕过任何请求处理程序的请求,因为任何 window.fetch() 调用,即使是在响应解析器内部的调用也会被拦截。
强烈建议在响应解析器中使用ctx.fetch()而不是常规的window.fetch(),因为它可以防止实际请求与MSW模拟定义相匹配。
您可能会有意地使用window.fetch(),例如当您需要执行稍后作为另一个响应解析器的一部分进行模拟的请求时。由常规window.fetch()发出的请求将受到模拟。
延迟响应
当没有明确提供延迟持续时间时,Mock Service Worker会在特定的模拟响应上使用一个随机的真实服务器响应时间。
ts
rest.delete('/post/:postId', (req, res, ctx) => {
return res(
/** 如果没有参数,会随机模拟一个真实的响应时间 */
ctx.delay(2000),
ctx.json({
message: `Post ${req.params.postId} successfully deleted!`,
}),
)
})
二进制响应类型
提供一个 BufferSource 对象给 ctx.body() 实用函数,将使用该缓冲区作为模拟响应的主体。二进制数据的支持允许在模拟响应中发送任何类型的媒体内容(图像、音频、文档)
ts
import { setupWorker, rest } from 'msw'
import base64Image from '!url-loader!../fixtures/image.jpg'
const worker = setupWorker(
rest.get('/images/:imageId', async (_, res, ctx) => {
// Convert "base64" image to "ArrayBuffer".
const imageBuffer = await fetch(base64Image).then((res) =>
res.arrayBuffer(),
)
return res(
ctx.set('Content-Length', imageBuffer.byteLength.toString()),
ctx.set('Content-Type', 'image/jpeg'),
// Respond with the "ArrayBuffer".
ctx.body(imageBuffer),
)
}),
)
worker.start()
从 post 请求的 body 中获取参数
ts
type IReqType = RestRequest<DefaultBodyType, PathParams<string>>;
type IResType = ResponseComposition<DefaultBodyType>;
type ICtxType = RestContext;
export const handlers = [
rest.post("/login", async (req: IReqType, res: IResType, ctx: ICtxType) => {
// 从 post 请求的 body 中获取参数(注意:req.body 已废弃)
const { username, password } = await req.json();
}
]