vite-plugin-mock-dev-server 拦截处理服务端数据
一、前言
哈喽,各位程序员老铁们,今天我要给大家介绍一个"贼溜"的 Vite 插件 ——vite-plugin-mock-dev-server
这个插件就像是咱们家门口的把门大爷,啥请求进出都得先经过它盘查一番。它最拿手的本事就是帮咱们在开发环境中拦路抢劫后端的数据请求,想咋改咋改,简直不要太爽!
你肯定遇到过这种情况:后端的接口迟迟不给力,你等得慌,或者后端给的数据格式咋看咋别扭,用着不得劲。这时候,用这个插件就能让你不再望天兴叹,自己做主,想咋整咋整!
相比其他的 Mock 方案,这款插件有啥特别之处嘛?那必须有!它不像有些插件需要在你的代码里面插针引线,搞得代码乱糟糟的。咱这个插件就像隐形人一样,悄咪咪地在后台工作,前端代码一点不用动,干活不落痕!
二、基础环境搭建
先来个热身运动,把基础环境搭起来。
1. 安装插件
# npm 安装
npm i -D vite-plugin-mock-dev-server
# 你要是用 yarn 的话
yarn add vite-plugin-mock-dev-server -D
# pnpm 的娃娃这样安装
pnpm add -D vite-plugin-mock-dev-server
安装完了,你就说:我安好了,这有啥难的嘛! 别急,咱这才刚开始呢!
2. 配置 vite.config.ts
配置起来也简单,把下面这段代码搁到你的 vite.config.ts 文件里就成:
import { defineConfig } from 'vite'
import { mockDevServerPlugin } from 'vite-plugin-mock-dev-server'
export default defineConfig({
plugins: [
// 其他插件...
mockDevServerPlugin({
// 默认就是 mock 目录,你要是有"小脾气"想换个地方,随你
// mockPath: 'mock',
// 将reload设置为false,避免构建后仍保持监听
reload: false,
// 开发环境下启用
prefix: '/api'
}),
],
// 如果你有代理配置,插件会自动读取
server: {
proxy: {
'/api': {
target: 'http://example.com',
changeOrigin: true
}
}
}
})
3. 创建 mock 目录及文件结构
来,"动动你的小手指",在项目根目录下创建一个 mock 文件夹,这就是咱们的"秘密基地"。
├── project
│ ├── mock/ # 这是咱们的mock目录
│ │ ├── user.mock.ts # 用户相关的mock
│ │ └── product.mock.ts # 产品相关的mock
│ ├── src/
│ ├── vite.config.ts
│ └── package.json
结构整好了,就可以开始撒欢儿写 mock 文件了!
三、基本使用方法
1. 创建基础 Mock 文件
咱们先来整一个最简单的例子,创建 mock/user.mock.ts 文件:
import { defineMock } from 'vite-plugin-mock-dev-server'
export default defineMock({
url: '/api/users',
method: 'GET',
body: {
code: 0,
message: '成功了,你看咋样!',
data: {
name: '老王',
age: 38,
hobby: '摸鱼'
}
}
})
就这么几行代码,你的第一个 mock 接口就整好了!简单得很哩!
2. 定义 API 接口
想要多整几个接口?没问题!你还可以在一个文件里定义多个接口,就像这样:
import { defineMock } from 'vite-plugin-mock-dev-server'
export default defineMock([
// 获取用户列表
{
url: '/api/users',
method: 'GET',
body: {
code: 0,
message: '获取用户列表成功',
data: [
{ id: 1, name: '二娃', age: 25 },
{ id: 2, name: '三娃', age: 22 },
{ id: 3, name: '四娃', age: 18 }
]
}
},
// 获取单个用户
{
url: '/api/users/:id',
method: 'GET',
body: (params) => {
// 这里可以根据params.id返回不同的用户
return {
code: 0,
message: `获取用户${params.query.id}成功`,
data: { id: params.query.id, name: `用户${params.query.id}`, age: 20 + Number(params.query.id) }
}
}
}
])
看看,配置动态路由参数也是这么简单,params 直接就把路径参数给你拿下来了,真是贼溜!
3. 数据模拟与响应设置
想要更地道一点?那就给响应加点料!
import { defineMock } from 'vite-plugin-mock-dev-server'
export default defineMock({
url: '/api/login',
method: 'POST',
// 延迟1秒返回,模拟网络慢一点
delay: 1000,
// 自定义响应状态码
status: 200,
// 可以使用函数,更灵活
body(req) {
const { username, password } = req.body
// 判断账号密码
if (username === 'admin' && password === '123456') {
return {
code: 0,
message: '登录成功!',
data: {
token: 'this_is_a_mock_token',
userInfo: {
name: '管理员',
avatar: 'https://example.com/avatar.jpg'
}
}
}
}
// 登录失败
return {
code: 1001,
message: '账号或密码错误,你再想想咧!',
data: null
}
}
})
这样一来,咱们就能模拟一个真实的登录流程,连账号密码验证都有了,要的就是这个贴心!
四、高级功能:CreateProxyInterceptor
现在咱们来整点硬菜,学习这个插件的拦截器功能。这个功能就像是变形金刚,可以把后端的真实接口数据变个样再给前端用。
1. createProxyInterceptor 原理与概念
createProxyInterceptor 是个啥捏?简单说,它就是一个能够拦路抢劫的功能,可以拦截真实后端的请求和响应,然后按照你的想法进行修改。
这个功能特别适合这种情况:后端接口已经有了,但返回的数据结构不太符合你的要求,或者你想在不改动后端的情况下,临时修改一些数据进行测试。
2. 实现拦截与修改请求
咱们来创建一个拦截器(完整代码,包含使用案例),把它放在 mock/interceptor.ts 文件中:
import * as zlib from "node:zlib";
import type { MockHttpItem } from "vite-plugin-mock-dev-server";
// 自定义 NextFunction 类型
type NextFunction = () => Promise<any>;
/**
* 类型定义:响应数据转换函数
* @param originalData 原始响应数据对象
* @param req 原始请求对象
* @return 修改后的数据对象
*/
export type ResponseTransformer = (originalData: any, req: Request) => any;
/**
* 创建一个代理拦截器配置
* @param url API路径
* @param transformer 响应数据转换函数
* @param options 额外配置选项
* @returns Mock配置对象
*/
export function createProxyInterceptor(
url: string,
transformer: ResponseTransformer,
options: {
method?: string | string[];
delay?: number;
statusCode?: number;
enabled?: boolean;
} = {}
): MockHttpItem {
return {
url,
method: options.method || "ANY",
delay: options.delay,
enabled: options.enabled,
status: options.statusCode || 200,
// 响应处理器
response(req: Request, res: Response, next: NextFunction) {
// 保存原始响应方法
const originalWrite = res.write;
const originalEnd = res.end;
// 用于收集响应数据的缓冲区
const chunks: Buffer[] = [];
// 重写write方法来收集数据
res.write = function (chunk, ...args) {
if (chunk) {
chunks.push(Buffer.from(chunk));
}
return true; // 不真正写入,仅收集数据
};
// 重写end方法来处理完整响应
res.end = function (chunk, ...args) {
try {
// 收集最后一块数据
if (chunk) {
chunks.push(Buffer.from(chunk));
}
// 合并所有数据块
const buffer = Buffer.concat(chunks);
// console.log(`[拦截] ${req.url} 收到数据,长度: ${buffer.length}字节`);
// 检测是否为gzip压缩数据
const isGzipped =
buffer.length > 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
if (isGzipped) {
// 处理压缩数据
handleGzippedResponse(buffer, req, res, originalEnd, transformer);
return true;
} else {
// 处理非压缩数据
handlePlainResponse(buffer, req, res, originalEnd, transformer);
return true;
}
} catch (error) {
console.error(`[错误] 处理 ${req.url} 响应时出错:`, error);
return originalEnd.apply(res, arguments);
}
};
// 继续请求处理流程
return next();
},
};
}
/**
* 处理gzip压缩的响应数据
*/
async function handleGzippedResponse(
buffer: Buffer,
req: Request,
res: Response,
originalEnd: Function,
transformer: ResponseTransformer
) {
try {
// 解压数据
const decoded = await gunzipAsync(buffer);
const decodedString = decoded.toString();
// 解析JSON
const originalData = JSON.parse(decodedString);
// console.log(`[拦截] ${req.url} 解压后的原始数据:`, originalData);
// 转换数据
const modifiedData = transformer(originalData, req);
// console.log(`[拦截] ${req.url} 修改后的数据:`, modifiedData);
// 准备发送修改后的响应
const modifiedString = JSON.stringify(modifiedData);
// 移除可能冲突的头信息
res.removeHeader("content-encoding");
res.removeHeader("transfer-encoding");
// 设置新的头信息
res.setHeader("Content-Type", "application/json");
res.setHeader("Content-Length", Buffer.byteLength(modifiedString));
// 发送修改后的数据
originalEnd.call(res, modifiedString);
} catch (error) {
console.error(`[错误] 处理 ${req.url} 的压缩响应时出错:`, error);
// 恢复原始响应
res.removeHeader("content-length");
res.removeHeader("content-encoding");
originalEnd.call(res, buffer);
}
}
/**
* 处理非压缩的响应数据
*/
function handlePlainResponse(
buffer: Buffer,
req: Request,
res: Response,
originalEnd: Function,
transformer: ResponseTransformer
) {
try {
const responseData = buffer.toString();
// 解析JSON
const originalData = JSON.parse(responseData);
console.log(`[拦截] ${req.url} 原始数据:`, originalData);
// 转换数据
const modifiedData = transformer(originalData, req);
console.log(`[拦截] ${req.url} 修改后的数据:`, modifiedData);
// 准备发送修改后的响应
const modifiedString = JSON.stringify(modifiedData);
// 移除可能冲突的头信息
res.removeHeader("transfer-encoding");
// 设置新的头信息
res.setHeader("Content-Type", "application/json");
res.setHeader("Content-Length", Buffer.byteLength(modifiedString));
// 发送修改后的数据
originalEnd.call(res, modifiedString);
} catch (error) {
console.error(`[错误] 处理 ${req.url} 的响应时出错:`, error);
// 恢复原始响应
res.removeHeader("content-length");
originalEnd.call(res, buffer);
}
}
/**
* 将基于回调的gunzip转换为Promise
*/
function gunzipAsync(buffer: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
zlib.gunzip(buffer, (err, decoded) => {
if (err) reject(err);
else resolve(decoded);
});
});
}
// 使用示例 1
// createProxyInterceptor(
// "/api/msa/traded-api-bdc/jackpot/acr/queryJackpotPopUp",
// (originalData, req) => {
// // 修改数据
// return {
// ...originalData,
// customField: "新增字段-奖池弹窗",
// timestamp: Date.now(),
// requestParams: req.body, // 可选:添加请求参数用于调试
// };
// },
// {
// method: "POST",
// delay: 0, // 可选:添加延迟
// }
// );
// 使用示例 2
// createProxyInterceptor(
// '/api/user/info',
// (originalData) => {
// // 修改用户信息
// return {
// ...originalData,
// data: {
// ...originalData.data,
// username: originalData.data.username + '(已修改)',
// vipLevel: 10, // 提升VIP等级
// balance: 99999 // 修改余额
// }
// }
// }
// )
// 使用示例 3
// [
// // 拦截第一个接口
// createProxyInterceptor(
// '/api/products/list',
// (originalData) => {
// // 修改产品列表
// return {
// ...originalData,
// data: originalData.data.map(product => ({
// ...product,
// price: product.price * 0.8, // 所有产品打八折
// inStock: true // 所有产品都有库存
// }))
// }
// }
// ),
// // 拦截第二个接口
// createProxyInterceptor(
// '/api/orders/recent',
// (originalData) => {
// // 修改订单状态
// return {
// ...originalData,
// data: originalData.data.map(order => ({
// ...order,
// status: 'completed', // 所有订单状态改为已完成
// paymentStatus: 'paid' // 支付状态改为已支付
// }))
// }
// },
// { method: 'GET' }
// )
// ]
这段代码的作用是拦截 createProxyInterceptor传入的url请求接口的响应,通过回调函数拿到接口原始响应数据,根据自己的场景进行加工,再返回给我们的请求.说人话就是使用vite-plugin-mock-dev-server进行接口数据拦截并修改。就像是你去买东西,老板说"今天商品打八折",但是这个折扣是你自己偷偷加上去的,老板并不知道!
3. 给大家看一个我实际使用的案例
import { defineMock } from "vite-plugin-mock-dev-server";
import { createProxyInterceptor } from "./proxyInterceptor";
export default defineMock([
{
enabled: false, // 是否启动对该接口的mock,在多数场景下,我们仅需要对部分接口进行 mock,而不是对所有配置了mock的请求进行全量mock,所以是否能够配置是否启用很重要 @default true
url: "/api/test",
method: "GET", // 默认值: ['GET', 'POST']
// type: 'json', // 类型: 'text' | 'json' | 'buffer' | string
headers: { "X-Custom": "12345678" },
cookies: { "my-cookie": "123456789" },
status: 200,
statusText: "ok", // 默认值: "OK"
delay: 100, // 延迟 100毫秒
body: () => ({ a: 1 }), // 类型: Body | (request: MockRequest) => Body | Promise<Body> type Body = string | object | Buffer | Readable
},
createProxyInterceptor(
"/api/jackpot/acr/queryJackpotPopUp",
(originalData, req) => {
if (originalData.data) {
originalData.data.deposit = 0; // 0:未转化 1:已转化
originalData.data.pledgeFirstDepositPrize = {
freeDrawPrizeCount: 22,
pledgeBonusRate: 0.022,
prizeAmount: 33,
};
}
// 修改后的数据
return {
...originalData,
requestParams: req.body, // 可选:添加请求参数用于调试
};
},
{
enabled: true, // 启用 mock
method: "POST",
delay: 150, // 可选:添加延迟
}
),
]);
这样一来,接口/api/jackpot/acr/queryJackpotPopUp的deposit和pledgeFirstDepositPrize字段就改成了我们 mock 的数据了!
测试用例-自测
这样,你就可以方便地测试前端在各种错误情况下的表现,比如loading状态、错误提示、重试逻辑等等,而不用等着后端去模拟这些错误。
到这里,我们就把vite-plugin-mock-dev-server的主要使用方法都介绍完了。有了这个神器,前端开发是不是顺畅多了?不用再被后端的进度卡脖子,想咋开发咋开发,痛快得很!
记住,这些mock配置只在开发环境生效,不会影响生产环境,所以尽情地撒欢儿去用吧!