Skip to content

Mock Service Worker

官网地址:mswjs.io/

mswjs:在浏览器和 Node 环境中轻松做 API Mocking

We also recommend using Mock Service Worker (MSW) to mock network requests, as this means your application logic does not need to be changed or mocked when writing tests.

mswjs 是一个同时兼容浏览器和 Node 的 API mocking 库,帮助开发者在 API 还没 ready 的情况下,通过 mock 数据进行完美开发。

mswjs 原理是在客户端使用 Service Worker API 拦截(网络层)实际请求实现 API 的请求、响应模拟。

Mocking API

mwsjs 支持两种类型的 API Mock,一个是 REST API,一个是 GraphQL API。 如果你知道两者的区别,或者压根就不知道 GraphQL,也没有关系,无脑看成 REST API 就行,因为我们 99.9% 的项目都是用 REST API。

请求流程图

该库注册了一个 Service Worker,通过 fetch 事件监听应用程序的出站请求,将这些请求指向客户端库,并发送模拟响应(如果有)给工作线程以便进行响应。

浏览器端

Mock Service Worker 在客户端是通过 Service Worker 做请求拦截的,因此这块我们要单独启动个服务器服务 来自 mwsjs Service Worker 文件中的代码请求 。

这块代码不需要我们手写,直接使用 npx msw init <PUBLIC_DIR> --save 指令就可以了。 这里的 <PUBLIC_DIR> 就是指项目的静态资源的存放目录,对 Next.js 来说,就是项目跟路径下的 public/ 目录

sh
npx msw init public/ --save

执行完成后,会在 public/ 下,看到多出一个 mockServiceWorker.js 文件。 mockServiceWorker.js** 不需要我们做任何修改,放在这里就行了。 **这个 worker 脚本会被注入到浏览器网页中,用于拦截请求。

注意,这里的 --save 是必须的,这个选项会在 package.json 文件中写一个 msw 字段。

json5
{
  // ...
  "msw": {
    "workerDirectory": "public"
  }
}

这样日后更新 msw 包的时候,public/ 目录下的 worker 脚本会自动更新。

配置 worker

接下来在创建 src/mocks/browser.js 文件:

ts
import { setupWorker } from 'msw'
import { handlers } from './handlers'

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;
};
// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...getHandler())

这一步将我们定义的 handlers 绑定到 mws 上(这时候定义的 Mock API 才与 mws 有了联系)。

我们导出 worker 实例。

启动 worker

worker 实例准备好后,就要在合适的时机启动 worker 了(worker.start()):

对 Next.js 项目而言,我们在 pages/_app.tsx 中引入:

tsx
import '@/styles/globals.css'
import type { AppProps } from 'next/app'

async function initMocks() {
	if (typeof window !== 'undefined') {
		const { worker } = await import('../mocks/browser')
		worker.start({
			quiet: false, // 是否禁止在控制台打印匹配的日志记录
			onUnhandledRequest: "bypass", // 对于没有 mock 的接口直接通过,原样执行
		})
	}
}

initMocks()

export default function App({ Component, pageProps }: AppProps) {
	return <Component {...pageProps}
	/>
}

完成以上步骤,启动项目后控制台有以下提示,说明 MSW 启动成功

Node 端

配置服务器

创建 src/mocks/server.js 文件:

ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers)

setupServer() 使用之前定义好的 handlers 创建一个服务器。

需要注意的是,非 DOM 环境下的请求 URL 都必须使用绝对地址(absolute request URLs)。

注意,不要使用 Fetch API! mws 是通过拦截 Node 的 http/https module 实现 API Mocking 的,因此 Node 中原生支持的 fetch API 会 Mock 失败的。

功能特性

条件响应

当响应解析器返回 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() 实用函数,将使用该缓冲区作为模拟响应的主体。 二进制数据的支持允许在模拟响应中发送任何类型的媒体内容(图像、音频、文档)

text
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();
	})
]

Context

Context 常用的就是:

status: 用于处理状态码。 delay: 用于延时,模拟真实的请求速度。 json: 用于返回 json 数据。 除此之外,还提供了:

set:用于设置 headers。 body:在 body 返回,不参与 Content-Type 转换,例如二进制数据。 text:Content-Type: text/plain,返回纯文本。 xml:Content-Type: text/xml,返回 XML 数据。 data:针对 GraphQL 操作的响应。 errors:针对 GraphQL 的错误返回。 fetch:可以理解为在拦截请求时,发送一个真实的请求,用于修正返回结果时使用。

结合Faker 造数据

MSW 只解决了模拟接口请求,但是不支持模拟数据,数据需要你自己造,一个制造假数据(但很真实)的库,名字叫 Faker.js。 注意一下,faker 是支持多国语的,在 import 时可以选择你需要的语言,例如这里使用中文

ts
import { faker } from "@faker-js/faker/locale/zh_CN";

rest.get('/api/user', (req, res, ctx) => {
	return res(
		ctx.status(200),
		ctx.delay(1000),
		ctx.json(
			Array.from({ length: 10 }).map(() => ({
				fullname: faker.person.fullName(),
				email: faker.internet.email(),
				avatar: faker.image.avatar(),
				address: faker.location.streetAddress(),
			}))
		)
	);
})

常见问题

在给项目安装msw,但是mock没有生效,一直404,这里放上我的调试过程。

在 public 文件夹下面的 mockServiceWorker 文件中加上打印,发现有打印内容,说明Service Worker是正常的 查看请求,发现url没有异常 因为项目在localhost之后加了一层url,对比之前的 mock 写法,发现是在react main文件配置的时候写错了,正确代码如下:

ts
if (ENV === 'local') {
	await worker.start({
		serviceWorker: {
			url: `/${[第二层路由]}/mockServiceWorker.js`,
		},
	});
}

Contributors

作者:Long Mo
字数统计:2k 字
阅读时长:7 分钟
Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs