Vite插件:vite-plugin-mock插件原理解析

1,248 阅读5分钟

背景介绍

在我们平常开发过程中,我们项目多数是前后端分离的项目,就需要和服务进行前后联调,需要服务给接口文档,然后我们再根据给的结构文档mock数据,在这之前呢我都是直接在页面中写死数据来进行模拟的,这不最近看了vite-plugin-mock这个插件。

插件功能介绍

vite-plugin-mock 是vbean团队开发的一个插件,目的就是在开发时拦截发送的请求,然后返回模拟的数据。用法就是 pnpm add vite-plugin-vite mockjs 两个库,vite-plugin-mock是强依赖mockjs库的。然后配置vite.config.js文件,然后在指定的mock文件夹下进行api的编写。

  1. 配置vite.config.js文件
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { viteMockServe } from 'vite-plugin-mock';

// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
  console.log('command, mode:', command, mode);
  return {
    plugins: [
      vue(),
      ViteAliases({
        prefix: '@',
      }),
      viteMockServe({ // vite-plugin-mock的配置项
        mockPath: 'mock',
        enable: true,
      }),
    ],
  };
});

viteMockServe 参数如下: 其中常用的就是mockPathignore,两个的默认值分别是mockPath='mock', ignore=true

{
    mockPath?: string; //mock的文件夹路径
    ignore?: RegExp | ((fileName: string) => boolean); //自动读取模拟.ts文件时,忽略指定格式的文件
    watchFiles?: boolean;//设置是否监视 mock .ts 文件中的更改
    enable?: boolean;//是否开启模拟
    ignoreFiles?: string[];
    configPath?: string;
}

2. 在创建mockPath指定的js文件(user.js),是相对于当前项目根目录的哈,然后在user.js中模拟网络请求,返回数据即可。

image.png

export default [
  {
    url: '/api/createUser',
    method: 'post',
    response: (req, res) => {
      console.log('body>>>>>>>>', req);
      return {
        code: 0,
        message: 'ok',
        data: null,
      };
    },
  },
];

  1. 运行网络进行请求该地址数据,最终得到模拟数据。当然在user.js文件中也可以使用mockjs来进行数据的创建。

image.png

插件原理介绍

  • 直接请求axios.get('/api/createUser') 是会请求开发服务器的地址的,即:http://localhost:5174/api/createUser,如果没有匹配上的就返回当前根地址页面。
  • 插件进行拦截开发服务器的请求,然后将mockPath下创建的模拟请求数据读取,然后进行匹配,如果有匹配上的就返回对应的数据,没用就返回默认内容。

image.png

插件复现

在完成插件复现之前我们要理清楚完成这个插件需要做哪些准备:

  • 我们需要一个函数/api去匹配开发服务器发送的请求。所以我们需要了解vite 插件api:configureServer 用于配置开发服务器的钩子。
  • 我们需要在插件中实现动态js文件的导入。导入mock目录(mockPath指定的路径)下的js文件。
  • 重新熟悉 fs、path模块。

configureServer 函数 插件 API | Vite 官方中文文档

是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件:

// 写法一
const myPlugin = () => ({ 
    name: 'configure-server', 
    configureServer(server) { 
        server.middlewares.use((req, res, next) => {
            // 自定义请求处理... 
        }) 
    }, 
})

//写法二
const myPlugin = () => ({
  name: 'configure-server',
  configureServer(server) {
    // 返回一个在内部中间件安装后
    // 被调用的后置钩子
    return () => {
      server.middlewares.use((req, res, next) => {
        // 自定义请求处理...
      })
    }
  },
})

其中server.middlewares.use函数类似于express/koa中的中间件,参数也是req、res、next。

复现代码

import fs from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
export default function customMockPlugin(options = {}) {
  const { enable = true, mockPath = 'mock' } = options;
  return {
    name: 'vite-plugin-custom-mock',

    async configureServer(server) {
      if (!enable) return; // 如果插件禁用,直接返回

      // 加载 mock 数据
      const mockData = await loadMockData(mockPath);
      if (mockData.length === 0) {
        console.warn('[vite-plugin-custom-mock] 没有找到 mock 数据');
        return;
      }

      // 拦截请求
      server.middlewares.use((req, res, next) => {
        const url = req.url.split('?')[0]; // 去掉查询参数
        const method = req.method.toLowerCase();

        // 查找匹配的 mock 数据
        const mock = mockData.find(
          item => item.url === url && item.method === method
        );
        if (mock) {
          console.log(`[vite-plugin-custom-mock] 拦截请求: ${method} ${url}`);
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify(mock.response(req, res)));
          return;
        }

        next(); // 如果没有匹配的 mock 数据,继续下一个中间件
      });
    },
  };
}

// 加载 mock 数据
async function loadMockData(mockPath) {
  const mockDir = path.resolve(process.cwd(), mockPath);

  if (!fs.existsSync(mockDir)) {
    console.warn(`[vite-plugin-custom-mock] mock 文件夹不存在: ${mockDir}`);
    return [];
  }

  const mockFiles = fs
    .readdirSync(mockDir)
    .filter(file => file.endsWith('.js'));

  const mockData = [];
  for (const file of mockFiles) {
    const filePath = path.join(mockDir, file);
    const fileUrl = pathToFileURL(filePath).href; // 转换为 file: 协议的 URL
    const module = await import(fileUrl); // 动态引入
    mockData.push(module.default);
  }
  return mockData.flat(Infinity);
}

上述代码实现的原理就是在指定的mockPath下将请求的api地址和模拟的数据进行匹配,如果匹配到了就调用其response函数,返回数据。具体项目工程截图如下:

image.png 上图是大概的内容,然后再ViteMock函数中就是复现的代码了,就是上面的ViteMock.js代码片段。我的这个插件是写在plugins目录下的,在vite.config.js文件中啊需要将其引入哦。

知识点总结

  • fs模块:
    • existsSync函数:是一个同步函数,返回boolean。判断一个路径是否存在。
    • readdirSync函数:是一个同步函数,返回一个数组。返回文件夹下的内容(文件和文件夹)
  • path模块:
    • resolve函数:是一个地址路径拼接函数,功能类似于linux中的cd命令。
    • join函数:是一个地址路径拼接函数,就是一个简单的拼接地址的函数(类似于字符串的拼接)。
  • url对象:
    • pathToFileURL函数(非常有用):是 Node.js 提供的一个工具函数,用于将文件路径转换为 file: 协议的 URL。它在处理文件路径时非常有用,尤其是在需要将路径传递给支持 URL 的 API(如 import())时。
      • 例子(动态加载项目本地文件):
          import { pathToFileURL } from 'url';
          import path from 'path';
      
          async function loadFile() {
            const filePath = path.resolve(process.cwd(), 'src', 'example.js'); // 解析为绝对路径
            const fileUrl = pathToFileURL(filePath).href; // 转换为 file: 协议的 URL
            const module = await import(fileUrl); // 动态引入
            console.log(module.default.message); // 输出: Hello from example.js!
          }
      
          loadFile();
      
  • process.cwd() 函数:返回当前执行进程的目录(即项目的根目录),在写第三方插件的时候推荐用这个路径。
  • configureServer函数:用于配置配置开发服务器的钩子函数。