基于msw的前端接口mock工具
在以往的项目中,数据mock比较简单,只需要mockjs的Mock.mock
拦截一下请求,或者直接使用Chrome扩展程序来覆盖某些url接口的响应。
我之前写过一个@shymean/mock-server的命令行工具,可以读取一个mock文件,然后快速启动一个mock服务器,用于支持App、小程序等非web开发环境下的接口mock。
但是在目前的系统中,业务使用websocket收发消息的场景较多,而ws造数据是一个比较复杂的事情,常规的mock工具没有办法满足需求。
经过一番调研,最终选择了mswjs,并成功在项目中成功应用起来。本文整理一下整个过程以及一些心得体会。
mock目标
支持多种资源的mock
首先是可以代理的接口能力,我希望前端应用中的网络请求都能够被mock代理,比如常规的Ajax、新一点的fetch,以及websocket和老接口的JSONP,尤其是websocket,目前项目中的构造ws消息需要进行很麻烦的操作步骤。
看起来,mock server和 service worker mock是比较好的方案,而传统的代理XHR对象的方式就比较局限。
mock server就是搭建一个代理服务器,真实服务器可以发送的消息,都可以由这个mock服务器发送。
service worker mock就是通过service worker拦截网络请求,然后返回代理的mock接口数据。
明显区分mock和真实接口
mockjs是通过修改原生的XHR对象,直接返回了对应的响应内容,对应的网络请求实际上并不会发送,因此无法在devtools的network面板看到,不方便查看请求参数和响应内容
Mock.mock(/posts\/list/, {
msg: 'ok',
code: 0,
data: {
name: "@cname"
},
})
由于目前我们的业务比较复杂,一个页面甚至需要请求10来个接口进行初始化,所以我希望可以在进入页面的时候,可以清晰地看到哪些接口是被mock的,哪些是直接走原始的服务器请求;同时,那些被mock的接口还是可以在开发者工具查看,统一调试体验。
基于代码,可以版本控制
这个mock工具最好是可以完全由js代码来控制,而不是扩展程序或者桌面应用之类的工具。
应用工具需要有一定的学习成本,甚至需要付费;还需要面对功能扩展和版本迭代等问题。
而代码的灵活性很高,很方便造数据和压力测试,且源文件是可以通过git版本管理,方便团队共享。我认为有了mock,可以快速查看某个接口返回的数据,对后续接手该业务的同事是更友好的
可以接入API文档
手动编写需要mock的接口,需要花费一定的时间精力。
理想情况下,mock工具应该是可以读取后端的接口定义文档如YAPI、Swagger等,这样可以自动生成接口mock代码。
不过目前AI编码也比较成熟了,我觉得这里的开发工作应该是比较低的。
mswjs
mswjs基于 JavaScript 的运行时请求拦截库,能拦截和处理浏览器或Node.js 环境中的 HTTP 和 WebSocket 请求。
它可模拟各种网络请求和响应场景,提供灵活的 API 来定义请求处理逻辑,使开发者能方便地模拟 Ajax、Fetch 和 WebSocket 的不同响应状态与数据。
在浏览器端,msw通过service worker的方式拦截网络请求并返回mock数据,这样可以满足我上面的几点需求
- 支持多种资源的mock
- 可以在调试面板看到对应的网络请求,只是响应变成了(from serviceWorker)
- 通过提供的api,编写代码进行mock,同时与mockjs等测试数据构建工具兼容良好
具体的使用可以参考官方文档,本文并不会有过多涉及。
vite接入msw演示
接下来演示一下在前端项目中接入msw进行mock的步骤
首先创建一个前端测试应用,这里使用vite
pnpm create vite
pnpm install
然后安装msw
pnpm install msw --save-dev
然后生成对应的worker文件,该命令会在public目录下生成一个mockServiceWorkder.js
的文件
npx msw init public/ --save
msw会在浏览器端自动注册这个worker
然后编写mock相关代码
// mocks/browser.js
import { setupWorker } from 'msw/browser'
import { http, HttpResponse } from 'msw'
import {Random} from 'mockjs'
export const handlers = [
http.get('https://example.com/user', () => {
return HttpResponse.json({
id: 'c7b3d8e0-5e0b-4b0f-8b3a-3b9f4b3d3b3d',
firstName: Random.cname(3),
lastName: 'Maverick',
})
}),
]
export const worker = setupWorker(...handlers)
然后在应用入口文件加载这个mock文件
async function enableMocking() {
// 通过环境变量区分,避免mock工具在非开发环境生效
if (process.env.NODE_ENV !== 'development') {
return
}
const ans = await import('../mocks/browser')
const { worker } = ans
return worker.start()
}
enableMocking().then(() => {
// react应用初始化
const app = (<App/>)
createRoot(document.getElementById('root')).render(
app
)
})
之后在应用中编写一个网络请求接口
async function fetchUser(){
// 这里的url就是上面handlers里面注册的那个url
const response = await fetch('https://example.com/user')
const user = await response.json()
console.log(user)
}
fetchUser()
然后就可以看见对应的响应被代理成功了
处理未mock的请求
默认没有被代理的接口,会在控制台通过console.warn
的方式提示(如上面截图所示)。
可以在handlers
添加一个新的handler,监听通配符*
,这样可以在控制台直接看到哪些未被mock的请求
// 添加一个通用的处理器来捕获所有未被 mock 的请求
http.all('*', async ({ request }) => {
console.log('⚠️ 未 mock 的请求:', request.url)
//
})
由于项目没有历史mock的数据,如果需要实现某个页面的所有接口都支持mock
- 方案一,进入页面,进行相关操作,打开控制台,查看network,挨个整理用到的网络请求,手动编写mock代码
- 方案二,进入页面,进行相关操作,通过一个自动化的工具在后台收集到所有没有被mock的工具,获取url和响应的映射表,自动生成mock代码
毫无疑问方案二的自动化操作可以节省很多人力
export class UnMockCollect {
record = {}
addUnMockRequest(url, method, response) {
console.log({ url, method, response })
this.record[url] = { url, method: method.toLowerCase(), response }
}
createHandlerByUrl(url) {
const row = this.record[url]
if (!row) {
console.warn(`url:${url}对应的请求未被收集,请检查`)
return ''
}
const tpl = `
http.${row.method}('${row.url}', () => {
return HttpResponse.json(${JSON.stringify(row.response, null, 4)})
})
`
return tpl
}
printHandlerByUrl(url) {
const code = this.createHandlerByUrl(url)
console.log(code)
}
printAllUrls() {
const keys = Object.keys(this.record)
console.log(keys)
}
printAllHandlers() {
const codes = []
for (const key of Object.keys(this.record)) {
const code = this.createHandlerByUrl(key)
codes.push(code)
}
console.log(codes.join(','))
}
}
export default new UnMockCollect()
然后创建一个通配符处理器
import UnMockCollect from '../utils/UnMockCollect'
if (needMockCollect) {
window.__un_mock_record = UnMockCollect
}
// 添加一个通用的处理器来捕获所有未被 mock 的请求
needMockCollect && http.all('*', async ({ request }) => {
// 定义需要跳过的后缀正则
console.log('debug ⚠️ 未 mock 的请求:', request.url)
if (request.headers.get('x-msw-bypass')) return
// 添加特殊标记,避免死循环
const headers = new Headers(request.headers)
headers.set('x-msw-bypass', 'true')
// 获取原始响应
const response = await fetch(request, { headers })
// 克隆响应以便多次读取
const clonedResponse = response.clone()
// 获取响应数据
const responseData = await clonedResponse.json()
UnMockCollect.addUnMockRequest(request.url, request.method, responseData)
// 返回原始响应
return new HttpResponse(JSON.stringify(responseData), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
})
})
在进入页面操作完成之后,就可以通过window.__un_mock_record.printAllHandlers()
来展示所有的mock代码,然后复制到msw中,再按照自己的需求修改响应内容就可以了。
mock websocket
msw可以充当ws客户端和服务端之间的代理,因此可以很灵活的mock
import { ws } from 'msw'
const url = `wss://ws.xxx.com`
const chat = ws.link(url)
chat.addEventListener('connection', ({ server, client }) => {
console.log('debug Intercepted a WebSocket connection:')
server.connect()
// 监听客户端发送的消息
client.addEventListener('message', (event) => {
event.preventDefault()
// server.send(event.data)
client.send(JSON.stringify({
msg: 'hello from server!'
}))
})
// 监听服务器返回的消息
server.addEventListener('message', (event) => {
event.preventDefault()
client.send(event.data)
})
}),
创建连接之后,可以通过编程的方式来模拟ws消息,比如定时推送、模拟消息并发等。
唯一的缺点是,msw的ws mock貌似也是通过修改WebSocket的实现来处理,对应的消息无法再network的ws面板看见消息发送的过程。
mock代码与应用代码分离
从上面的演示可以看出,实际上mock代码跟应用代码是在一起的,enableMocking
开启之后初始化应用代码,这样mock代码更新之后,还会触发热更新操作。
在实际开发中,由于我们的业务比较复杂,
- 项目本身有service worker,用于接口缓存
- 项目规模比较大,不希望触发额外的热更新
- 需要有一个机制快速开启和关闭全局mock
最终采用的方案是通过外部脚本的方式加载mock代码,这个思路参考了react scan的一键集成方案。
要在项目中使用react scan,只需要引入一个外链js文件
<script src="https://unpkg.com/react-scan/dist/auto.global.js"/>
参考这个思路,我们的mock代码也可以通过这个方式引入
构建iife mock脚本
首先将当前的mock代码打个包,因此需要构建一个入口文件
// src/entry.js
import {worker} from '../mocks/browser'
worker.start()
console.log('debug inject mock data')
直接用vite构建这个文件模式
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: './src/entry.js',
name: 'mockWorker',
formats: ['iife'],
fileName: 'mock-worker'
}
}
})
然后执行npm run build
,会在dist目录下生成mock-worker.iife.js
,同时public下的mockServiceWorker.js
worker文件也会自动复制过去。
然后开启一个静态资源服务器,让外部可以访问到这两个文件
php -S localhost:3001
在修改mock代码后,需要重新构建这个iife的js文件,由于代码本身没有啥外部依赖,vite构建速度也比较快,基本上可以满足业务快速调试的需求。
项目接入
由于我们项目已有service worker脚本,名字为service-worker.js
,为了减少接入mock对当前项目的改动,在项目自己的woker中,可以通过importScripts
加载mockServiceWorker
// service-worker.js
needMock && importScripts("http://localhost:3001/mockServiceWorker.js")
msw在worker.start()
的时候,会自动注册mockServiceWorker.js
这个文件,现在我希望它注册service-worker.js
这个文件,因此需要修改一下配置,然后重新构建
window.__init_mock_service__ = async function (test = true) {
worker.start({
serviceWorker: {
url: '/service-worker.js'
}
})
}
最后,在项目文件的html中,通过环境变量的方式,加载上面构建好的mock 脚本文件即可,比如我们项目使用的umi,就可以配置headScripts
// .umirc.js
headScripts:[
needMock && 'http://localhost:3001/mock-worker.iife.js'
],
正常情况下worker.start
是一个异步的流程,因此需要对项目初始化的做一点改造,这也是不如react-scan的一点,毕竟还是对业务代码有一次性改动的侵入
if (process.env.NODE_ENV === 'development') {
if(window.__init_mock_service__) {
await window.__init_mock_service__()
}
}
// 原始的init逻辑
这样,在加载我们自己的项目代码时,就可以正常使用mock了。
小结
按照上面的思路,我在项目中实际使用了一段时间,确实还是能提升不少开发效率
- 对业务本身不是很熟,构造数据需要浪费很多时间,直接修改响应会快很多
- 通过编程式的mock,可以避免常规如chrome扩展程序等基于JSON的mock工具带来的不灵活问题,可以实现非常灵活的mock
目前使用下来感觉还是不错的,尤其是在websocket消息和海量数据性能压测方面,后续遇到新的问题或者有新的想法,会继续更新本文。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
