使用vite-plugin-mock-dev-server插件拦截处理服务端数据

1,053 阅读8分钟

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/queryJackpotPopUpdepositpledgeFirstDepositPrize字段就改成了我们 mock 的数据了!

测试用例-自测

这样,你就可以方便地测试前端在各种错误情况下的表现,比如loading状态、错误提示、重试逻辑等等,而不用等着后端去模拟这些错误。

到这里,我们就把vite-plugin-mock-dev-server的主要使用方法都介绍完了。有了这个神器,前端开发是不是顺畅多了?不用再被后端的进度卡脖子,想咋开发咋开发,痛快得很!

记住,这些mock配置只在开发环境生效,不会影响生产环境,所以尽情地撒欢儿去用吧!