网站首页 > 技术文章 正文
mswjs 是什么?
msw 是 Mock Service Worker的简写,是一个基于 Service Worker 实现的 API 模拟库,允许您编写与客户端无关的模拟。
当然它也是不是完美的,因为其基于 Service Worker 实现,所以其运行在 HTTPS 环境,或者在开发环境中运行于 localhost,无法在其他 HTTP 环境中使用详见mdn,官方其他限制说明
核心功能
- 拦截网络请求并返回模拟数据
- 支持 REST API、GraphQL API、WebSocket API 的模拟
- 可用于 浏览器环境 和 Node 环境
- 支持开发环境和测试环境使用, 其他模拟请求的库只能本地使用这个部署到线上后依然可以!
与其他库相比优势是什么?
- 使用标准的请求/响应 API
- 可以在开发工具中查看请求和响应
- 可以模拟网络延迟和错误情况
- 可以精确控制 API 响应
- 支持 headers、status code 等HTTP特性
- 可以模拟各种网络情况
- 不需要修改应用代码或配置代理,不需要启动真实服务器
- 提供 TypeScript 支持,有更好的类型提示
- 支持集成测试和端到端测试
使用场景
- 前端开发时后端 API 还未就绪
- 需要模拟复杂的 API 响应场景
- 编写可靠的前端测试用例
- 需要在离线环境开发调试
mswjs 使用
本次主要在浏览器环境中模拟 REST API、WebSocket API 请求, GraphQL API 如有需要后续补充!
我会使用 vite 初始化一个 vue+ts 的项目 猛击直达 github 仓库
安装 mswjs
执行 pnpm install msw@latest 安装 msw
执行 npx msw init ./public --save 创建 Worke 文件 ./public 是 mockServiceWorker.js 文件的存放目录
--save 参数会在 package.json 增加 mockServiceWorker.js 所在的目录,在下次安装 msw 依赖时会自动将 mockServiceWorker.js 拷贝到对应的目录中。
安装 @mswjs/data
@mswjs/data 是一个用于构建和管理模拟数据的库,提供了一种简单的方法来创建和管理这些模拟的后端数据。通常与 msw搭配使用。
主要功能和特点包括:
- 定义数据模型,类似于 ORM 中的实体定义。这让你能在前端定义数据库表的结构。
- 支持常见的数据库操作,如创建、读取、更新和删除(CRUD)一对多、多对对查询。
- 内存存储,数据存储在内存中,这适合在开发和测试环境中快速迭代,不会影响到真实的数据库。
执行 pnpm i @mswjs/data 安装,然后在后续的集成环节使用它。
集成 mswjs
在 src/mock 文件夹下创建下图中的对应文件
database.ts 数据模拟
使用 factory 创建一个 user 表同时导出 db 变量让外部使用,然后填充一些默认数据
import { factory, primaryKey } from '@mswjs/data'
export const db = factory({
user: {
id: primaryKey(String),
email: String,
password: String,
nickName: String,
accountType: String,
role: String,
updatedAt: String,
createdAt: String,
avatar: String,
},
})
// ===================================== 填充一些默认数据 =====================================
db.user.create({
id: '1bofj153qd3188su7qb00u5n6oh',
email: 'admin@qq.com',
password: '123456',
nickName: 'kkf2Pg',
accountType: '01',
role: '01',
updatedAt: '2024-07-28 22:04:04',
createdAt: '2024-07-28 22:04:04',
avatar: 'https://api.dicebear.com/7.x/bottts-neutral/svg?seed=kkf2Pg&size=64',
})
db.user.create({
id: '1n3o6n1qh7d2uywa45y8100xweg0w',
email: 'test@qq.com',
password: '123456',
nickName: 'kklpCj',
accountType: '01',
role: '01',
updatedAt: '2024-07-21 13:28:33',
createdAt: '2024-07-21 13:28:33',
avatar: 'https://api.dicebear.com/7.x/bottts-neutral/svg?seed=kklpCj&size=64',
})
utils.ts 工具函数
import { HttpResponse } from 'msw'
// 获取基础 path,需要部署到 github pages , mswjs-demo 仓库名
export function getBasePath() {
return import.meta.env.PROD ? '/mswjs-demo' : ''
}
// 获取 api 的路径
export function getApiUrl(path: string) {
return `${getBasePath()}/${path}`
}
// 返回的数据格式
export function sendJson(code: number, data: any, msg: string = '') {
const info = { code, data, msg }
return HttpResponse.json(info, { status: 200 })
}
export function paginate<T>(table: T[], page = 1, pageSize = 10) {
const offset = (page - 1) * pageSize
return table.slice(offset, offset + pageSize)
}
handlers.ts 模拟请求
使用 mswjs 支持 REST API 风格的请求, 其中 all 函数表示任何类型的请求都会被拦截官方文档直达
下边我们看一下如何使用 mswjs 实现用户 crud 的模拟。
import type { UserInfo } from '@/api/user'
import { http } from 'msw'
import { v4 as uuidv4 } from 'uuid'
import { db } from './database'
import { getApiUrl, paginate, sendJson } from './utils'
export const userHandlers = [
// 获取用户列表
http.post(getApiUrl('api/user/getList'), () => {
const allList = db.user.getAll()
/**
* paginate 函数需要传入一个数组,返回一个分页后的数组
* 如果需要分页,需要在请求体中传入 pageNo 和 pageSize ,demo 就不做了
*/
const data = {
list: paginate(allList),
pageNo: 1,
pageSize: 10,
total: allList.length,
}
// sendJson 统一返回数据格式的函数
return sendJson(0, data)
}),
// 创建用户
/**
* <never, Omit<UserInfo, 'id'>> 表示:Params、RequestBodyType、ResponseBodyType、RequestPath 类型
*/
http.post<never, Omit<UserInfo, 'id'>>(getApiUrl('api/user/create'), async ({ request }) => {
const newUser = await request.json()
// 向 user 表中添加一个用户数据
const user = db.user.create({
id: uuidv4(),
...newUser,
})
return sendJson(0, user)
}),
// 删除用户
http.delete(getApiUrl('api/user/:id'), ({ params }) => {
const { id } = params
// 获取所有用户数据
const userList = db.user.getAll()
// 检查用户是否存在
if (userList.some(item => item.id === id)) {
// 根据 id 删除用户数据
const user = db.user.delete({
where: {
id: { equals: id as string },
},
})
return sendJson(0, null, `用户 ${user?.nickName} 删除成功!`)
}
else {
return sendJson(-1, null, '用户不存在!')
}
}),
// 更新用户
http.post<never, UserInfo>(getApiUrl('api/user/update'), async ({ request }) => {
const newUser = await request.json()
// 获取所有用户数据
const userList = db.user.getAll()
// 检查用户是否存在
if (userList.some(item => item.id === newUser.id)) {
// 根据 id 更新用户数据
const updatedUser = db.user.update({
where: { id: { equals: newUser.id } },
data: newUser,
})
return sendJson(0, updatedUser, '更新成功!')
}
else {
return sendJson(-1, null, 'User not found')
}
}),
]
export const allHandlers = [...userHandlers]
getApiUrl 因为这个 demo 最终需要部署到 gtihub pages 上,所以需要对 url 的路径进行一些处理!
index.ts 入口文件
导出加载 mswjs 的 initMswWorker 函数
import { setupWorker } from 'msw/browser'
import { allHandlers } from './handlers'
import { getBasePath } from './utils'
export const worker = setupWorker(...allHandlers)
export function initMswWorker() {
worker.start({
/**
* onUnhandledRequest 指定如何处理未匹配的请求
* warn:打印警告但按原样执行请求。
* error:打印错误并停止请求执行。
* bypass:不打印任何内容,按原样执行请求。
*/
onUnhandledRequest: 'bypass',
serviceWorker: {
/**
* 指定 `mockServiceWorker` 文件的路径,路径不正确请求会 405 错误
* 因为这里最后需要把项目部署到 github pages 上 所以需要使用 `getBasePath`函数获取正确的路径
*/
url: `${getBasePath()}/ats/mockServiceWorker.js`,
},
})
}
然后在 main.ts 中引入
加载成功控制台会有如下提示:
加载失败控制台会有如下提示:
实现用户的 crud
api 请求
这里的 api 请求使用 axios 封装 猛击直达 github
crud 的页面
crud 的页面使用 @opentiny/vue 组件库构建, 具体代码如下
<script setup lang="ts">
import type { UserInfo } from '@/api/user'
import { getUserList, addUser, removeUser, updateUser } from '@/api/user'
import { ref } from 'vue'
import { TinyNotify } from '@opentiny/vue'
import { v4 as uuidv4 } from 'uuid'
import dayjs from 'dayjs'
const userList = ref<Array<UserInfo>>([])
function queryUserList() {
const opts = {
nickName: '',
email: '',
pageSize: 10,
pageNo: 1,
}
getUserList(opts).then((res) => {
if (res.code === 0) {
userList.value = res.data.list
TinyNotify({ type: 'success', message: '获取成功!' })
}
})
}
queryUserList()
function createUser() {
const code = uuidv4().replaceAll('-', '').substring(0, 8)
const info = {
email: `${code}@qq.com`,
password: '123456',
nickName: code,
accountType: '01',
role: '01',
updatedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
avatar: `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${code}&size=40`,
}
addUser(info).then((res) => {
if (res.code === 0) {
queryUserList()
TinyNotify({ type: 'success', message: '新增成功!' })
}
})
}
function editUser(user: UserInfo) {
const info = {
...user,
nickName: uuidv4().replaceAll('-', '').substring(0, 8),
updatedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
}
updateUser(info).then((res) => {
if (res.code === 0) {
queryUserList()
TinyNotify({ type: 'success', message: '编辑成功!' })
}
})
}
function delUser(id: string) {
removeUser({ id }).then((res) => {
if (res.code === 0) {
queryUserList()
TinyNotify({ type: 'success', message: '删除成功!' })
}
})
}
const toolbarButtons = ref([
{
code: 'getList',
name: '查询'
},
{
code: 'add',
name: '新增'
},
{
code: 'edit',
name: '编辑'
},
{
code: 'delete',
name: '删除'
},
])
function toolbarButtonClickEvent({ code, $grid }: { code: string, $grid: Record<string, any> }) {
const data = $grid.getSelectRecords(true)
switch (code) {
case 'getList':
queryUserList()
break
case 'add':
createUser()
break
case 'edit': {
if (data.length === 0) {
TinyNotify({ type: 'warning', message: '请至少选中一条记录' })
}
if (data.length > 1) {
TinyNotify({ type: 'warning', message: '每次只能编辑一条记录' })
}
editUser(data[0])
break
}
case 'delete': {
if (data.length === 0) {
TinyNotify({ type: 'warning', message: '请至少选中一条记录' })
}
if (Array.isArray(data)) {
data.forEach((item) => {
delUser(item.id)
})
}
break
}
}
}
</script>
<template>
<div>
<tiny-grid :data="userList" border @toolbar-button-click="toolbarButtonClickEvent">
<template #toolbar>
<tiny-grid-toolbar :buttons="toolbarButtons"></tiny-grid-toolbar>
</template>
<tiny-grid-column type="index" width="60"></tiny-grid-column>
<tiny-grid-column type="selection" width="50"></tiny-grid-column>
<tiny-grid-column field="nickName" title="昵称"></tiny-grid-column>
<tiny-grid-column field="email" title="邮箱"></tiny-grid-column>
<tiny-grid-column field="role" title="角色"></tiny-grid-column>
<tiny-grid-column field="avatar" title="头像">
<template #default="data">
<tiny-image style="width: 60px;" :src="data.row.avatar"></tiny-image>
</template>
</tiny-grid-column>
<tiny-grid-column field="accountType" title="账户类型"></tiny-grid-column>
<tiny-grid-column field="createdAt" title="创建时间"></tiny-grid-column>
<tiny-grid-column field="updatedAt" title="更新时间"></tiny-grid-column>
</tiny-grid>
</div>
</template>
<style scoped></style>
查看 curd 效果
获取列表
增加用户:这里偷懒不做 ui 直接加数据
编辑用户:这里也偷懒了不做 ui 直接改数据
删除用户
handlers.ts
在 src/mock/handlers.ts 中增加 wsHandlers 然后在 allHandlers 中统一导出
handlers 的代码不多,我们简单看一下!
首先是创建 websocket
创建使用 ws.link() 方法如下:
const wsChat = ws.link('ws://wsChat') 创建了一个模拟的 websocket 连接 ws://wsChat 是客户端连接的地址。
ws 类似于 http、wss 对应 https
ws.link() 执行后返回一些方法和变量 猛击查看官方文档
- clients: clients: Set<WebSocketClientConnectionProtocol> 返回所有客户端连接
- addEventListener:可用于监听客户端连接、关闭等操作,下边会演示如何使用。
- broadcast:向所有已连接的客户端发送数据
- broadcastExcept:向除了当前客户端外的所有客户端发送消息
监听客户端连接
在 wsHandlers 中使用 chat.addEventListener('connection', ({ client }) => {}) 监听连接,client 是对应的客户端连接
// 监听 ws 连接
wsChat.addEventListener('connection', ({ client }) => {
// 首次连接向客户端发送连接成功的消息
client.send(createWsMsg('连接成功!'))
// 监听客户端发送的消息
client.addEventListener('message', (event) => {
// 向客户端发送消息
client.send(createWsMsg())
// 除了发送数据的客户端,所有已连接的客户端都将接收到发送的数据
wsChat.broadcastExcept(client, createWsMsg(`这是一条广播消息除了发送数据的客户端都会收到!${event.data}`))
// 所有已连接的客户端都将接收到发送的数据
wsChat.broadcast(createWsMsg('这是一条广播消息所有用户都会收到!'))
console.warn('wsChat 收到的消息:', event.data)
})
// 监听客户端断开连接
client.addEventListener('close', () => {
wsChat.broadcast(createWsMsg(`客户端断开连接${client.id}`))
})
}),
client.id 每次连接都会生成新的 id 可以来做一些有意思的事情
createWsMsg 函数定义了返回的数据格式
function createWsMsg(text?: string) {
return JSON.stringify({
uid: uuidv4(),
name: `wsChat`,
data: text ?? `这是 wsChat 返回的消息 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`,
})
}
websocket 连接页面
使用 vueuse useWebSocket 来连接 websocket 发送消息代码如下:
<script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
import { v4 as uuidv4 } from 'uuid'
import { ref } from 'vue'
interface WsDataItem {
uid: string
name: string
data: string
}
const dataList = ref<Array<WsDataItem>>([])
const { status, data, send, open, close } = useWebSocket('ws://wsChat', { onMessage: () => {
if (data.value) {
dataList.value.push(JSON.parse(data.value))
}
} })
open()
const msgText = ref('')
const userId = uuidv4().replaceAll('-', '').substring(0, 6)
// 发送消息
function sendMsg() {
const info = {
uid: uuidv4(),
name: `用户${userId}`,
data: msgText.value,
}
// 将数据添加进列表
dataList.value.push(info)
// 发送消息
send(JSON.stringify(info))
// 发送消息后清空输入框
msgText.value = ''
}
</script>
<template>
<tiny-card title="模拟 WebSocket 请求" style="width: 100%;margin-top: 20px;" status="success">
<p>状态: {{ status }}</p>
<p style="margin: 10px 0;">
数据:
</p>
<ul>
<li v-for="item in dataList" :key="item.uid" style="margin-bottom: 8px;">
{{ item.name }} : {{ item.data }}
</li>
</ul>
<div style="display: flex;">
<tiny-input v-model="msgText" placeholder="请输入要发送的消息" />
<tiny-button @click="sendMsg">
发送
</tiny-button>
<tiny-button v-if="status !== 'OPEN'" @click="open">
打开连接
</tiny-button>
<tiny-button v-else @click="close()">
关闭连接
</tiny-button>
</div>
</tiny-card>
</template>
<style lang='scss' scoped></style>
查看效果
发送消息
监听客户端连接关闭
mswjs 是怎么拦截请求的
mswjs 实现 nodejs 与浏览器的拦截请求的方式并不相同,我们简单了解其底层实现即可(作者没有深入研究)!
浏览器
浏览器的拦截请求是基于 Service Worker 实现
self.addEventListener('fetch', function (event) {...}) 是 Service Worker 中的一个事件监听器,用于拦截网页发出的网络请求并对其进行处理。
- self: 代表当前的 Service Worker 实例。
- addEventListener: 添加一个事件监听器,监听特定的事件。
- fetch: 事件类型,当网页发起网络请求时触发。
- event: 表示 fetch 事件的对象,包含与该请求相关的信息(如 URL 等)。 可以通过 event.request 获取具体的请求信息。 使用 event.respondWith(response) 可以自定义返回的响应。
nodejs
nodejs 的请求拦截官方实现了一个低级的网络拦截库 @mswjs/interceptors
实现了对 HTTP、WebSocket 协议的拦截
FAQ
如果遇到问题
- 查看官方调试章节
- 在 github issue 寻求帮助
如何自定义响应信息?
使用 msw 导出的 HttpResponse 函数 具体猛击查看官方文档
其他
如果你在使用 vite 且只需要本地模拟请求 vite-plugin-mock-dev-server 或许也是不错的选择
原文链接:https://juejin.cn/post/7445926398400102440
猜你喜欢
- 2024-12-17 WASM,传统前端的拯救者? 誉言aigc50前端全能版中文版软件免费版
- 2024-12-17 盘点程序员之间的那些鄙视链 程序员中的鄙视链
- 2024-12-17 6 万颗星,图片和视频秒变代码,让前端程序员下岗的开源项目
- 2024-12-17 通往前端高级程序员必须掌握的JavaScript引擎精华
你 发表评论:
欢迎- 509℃几个Oracle空值处理函数 oracle处理null值的函数
- 508℃Oracle分析函数之Lag和Lead()使用
- 499℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 495℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 486℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 480℃【数据统计分析】详解Oracle分组函数之CUBE
- 460℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 460℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (61)
- 前端数组 (73)
- 前端定时器 (47)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)