在前端开发过程中,经常会遇到后端数据缺失或后端服务尚未就绪的情况。此时,我们可以通过mock数据来模拟真实接口,确保开发工作不受影响。
接下来介绍下企业级 Vue3 + Vite 项目实战中vite-plugin-mock 的最佳实践方案。
一、先说适用范围与局限
vite-plugin-mock 原理 :在 Vite Dev Server(Connect 中间件层)拦截请求,只在 vite dev生效,不能用于 Vitest 单元测试或 Cypress E2E。
✅ 适合:前端独立开发、快速原型、无测试需求的中小型/中型项目
⚠️ 不适合:需要跨环境一致 mock(Vitest/Cypress)、需要模拟网络超时/断网等极端场景(建议用 MSW)
二、安装依赖
pnpm add -D vite-plugin-mock mockjs @types/mockjs
# 如果要用生产环境演示 mock(可选)
pnpm add -D vite-plugin-mock/client
三、推荐目录结构(贴合你的企业级模板)
my-enterprise-app/
├── mock/ # ✅ 根目录,与 src 平级
│ ├── index.ts # 统一导出所有模块(供插件读取)
│ ├── types.ts # Mock 相关类型定义
│ ├── utils/
│ │ ├── helper.ts # 分页/延迟/响应包装
│ │ └── db.ts # 简易内存数据库(CRUD 模拟)
│ ├── fixtures/
│ │ └── users.ts # 固定基准数据
│ └── modules/
│ ├── user.ts # 用户模块接口
│ └── order.ts # 订单模块接口
├── src/
│ ├── services/request/http.ts # Axios 实例
│ └── ...
├── vite.config.ts
├── .env.development
└── .env.mock # 开启 mock 的环境文件
四、vite.config.ts 配置(关键)
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'
import path from 'node:path'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
// 通过环境变量控制是否启用 mock
const enableMock = env.VITE_USE_MOCK === 'true' && command === 'serve'
return {
plugins: [
vue(),
viteMockServe({
mockPath: 'mock/modules', // mock 模块目录
enable: enableMock, // 按环境开关
logger: true, // 控制台打印拦截日志
watchFiles: true, // 修改 mock 文件热更新
// supportTs: true --- v3 默认支持,可不写
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
port: 3000,
// ⚠️ 重要:proxy 不能抢走 mock 要拦截的路径
// 如果配了 proxy['/api'],要确保 mock 先匹配
proxy: {
// '/api': { target: 'http://localhost:8080', changeOrigin: true }
// 建议:mock 开启时注释掉对应 /api proxy,或只 proxy 非 mock 路径
},
},
}
})
.env.development(联调后端):
VITE_USE_MOCK=false
VITE_API_BASE_URL=http://localhost:8080
.env.mock(前端独立开发):
VITE_USE_MOCK=true
VITE_API_BASE_URL= # 留空!vite-plugin-mock 拦截相对路径 /api/xxx
五、Axios 适配要点
vite-plugin-mock 拦截的是相对路径请求 ,所以 mock 模式下 baseURL要为空或 /:
// src/services/request/http.ts
import axios from 'axios'
import { loadEnv } from 'vite'
const isMock = import.meta.env.VITE_USE_MOCK === 'true'
const http = axios.create({
baseURL: isMock ? '' : import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
})
// 请求/响应拦截器照常写(token 注入、错误统一处理)
http.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers!.Authorization = `Bearer ${token}`
return config
})
http.interceptors.response.use(
(res) => res.data, // 根据你的后端结构拆包
(err) => Promise.reject(err)
)
export default http
⚠️ 常见坑 :mock 开着时 Axios
baseURL还配成http://localhost:8080,请求变成绝对路径,Vite 中间件拦截不到,mock 失效。
六、Mock 核心代码(类型安全 + 业务结构)
类型定义
// mock/types.ts
import type { MockMethod } from 'vite-plugin-mock'
export interface ApiResp<T = unknown> {
code: number
data: T
message: string
}
export type AppMockMethod = MockMethod
工具函数(分页 + 响应包装)
// mock/utils/helper.ts
import type { ApiResp } from '../types'
export function success<T>(data: T, message = 'ok'): ApiResp<T> {
return { code: 200, data, message }
}
export function fail(message: string, code = 400): ApiResp<null> {
return { code, data: null, message }
}
/** 简易内存分页 */
export function paginate<T>(
list: T[],
page = 1,
pageSize = 20
): { items: T[]; total: number } {
const start = (page - 1) * pageSize
return {
items: list.slice(start, start + pageSize),
total: list.length,
}
}
内存数据库(模拟 CRUD 状态变更)
// mock/utils/db.ts
import Mock from 'mockjs'
import type { User } from '@/domain/user/types' // 复用你领域层类型
export let userDb: User[] = Mock.mock({
'list|30': [
{
'id': '@guid',
'name': '@cname',
'phone': /1[3-9]\d{9}/,
'email': '@email',
'role': ['admin', 'user', 'editor'],
'status|1': ['active', 'disabled'],
'createdAt': '@datetime',
'updatedAt': '@datetime',
},
],
}).list
用户模块 Mock(完整 CRUD + 分页 + 错误模拟)
// mock/modules/user.ts
import type { MockMethod } from 'vite-plugin-mock'
import { success, fail, paginate } from '../utils/helper'
import { userDb } from '../utils/db'
import type { User, CreateUserDto } from '@/domain/user/types'
export default [
// 列表 + 分页 + 关键字搜索
{
url: '/api/users',
method: 'get',
response: ({ query }: any) => {
const page = Number(query.page) || 1
const pageSize = Number(query.pageSize) || 20
const keyword = (query.keyword as string) || ''
let filtered = userDb
if (keyword) {
filtered = filtered.filter(
(u) => u.name.includes(keyword) || u.phone.includes(keyword)
)
}
const { items, total } = paginate(filtered, page, pageSize)
return success({ items, total, page, pageSize })
},
},
// 详情
{
url: '/api/users/:id',
method: 'get',
response: ({ query, req }: any) => {
// path-to-regexp 匹配到的 param 在 req.params
const id = req.params?.id || query.id
const user = userDb.find((u) => u.id === id)
if (!user) return fail('用户不存在', 404)
return success(user)
},
},
// 新增
{
url: '/api/users',
method: 'post',
response: ({ body }: any) => {
const dto = body as CreateUserDto
if (!dto.phone) return fail('手机号不能为空')
const newUser: User = {
id: crypto.randomUUID(),
name: dto.name || '新用户',
phone: dto.phone,
email: dto.email || '',
role: dto.role || 'user',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
userDb.unshift(newUser)
return success(newUser, '创建成功')
},
},
// 编辑
{
url: '/api/users/:id',
method: 'put',
response: ({ body, req }: any) => {
const id = req.params?.id
const idx = userDb.findIndex((u) => u.id === id)
if (idx === -1) return fail('用户不存在', 404)
userDb[idx] = { ...userDb[idx], ...body, updatedAt: new Date().toISOString() }
return success(userDb[idx], '更新成功')
},
},
// 删除
{
url: '/api/users/:id',
method: 'delete',
response: ({ req }: any) => {
const id = req.params?.id
userDb = userDb.filter((u) => u.id !== id)
return success(null, '删除成功')
},
},
// 批量删除
{
url: '/api/users/batch-delete',
method: 'post',
response: ({ body }: any) => {
const { ids } = body as { ids: string[] }
userDb = userDb.filter((u) => !ids.includes(u.id))
return success(null, '批量删除成功')
},
},
] as MockMethod[]
统一入口(供 mockPath 自动扫描,也可手动 import)
// mock/index.ts
// 如果只是让插件扫描 modules/* 可留空导出
// 如需手动注册或有额外逻辑可在这里聚合
export {}
七、package.json 启动脚本
{
"scripts": {
"dev": "vite --mode development",
"dev:mock": "vite --mode mock"
}
}
运行 pnpm dev:mock→ 读取 .env.mock→ VITE_USE_MOCK=true→ mock 启动
运行 pnpm dev→ 读取 .env.development→ mock 关闭 → 请求走 proxy 到真实后端
八、可选:生产环境演示用 Mock(慎用)
vite-plugin-mock 支持打包进生产构建(仅用于演示/展示环境):
// mockProdServer.ts --- 项目根目录
import { createProdMockServer } from 'vite-plugin-mock/client'
import userMock from './mock/modules/user'
import orderMock from './mock/modules/order'
export function setupProdMockServer() {
createProdMockServer([...userMock, ...orderMock])
}
// src/main.ts --- 顶部注入
if (import.meta.env.PROD && import.meta.env.VITE_USE_MOCK === 'true') {
import('../mockProdServer').then(({ setupProdMockServer }) => {
setupProdMockServer()
})
}
⚠️ 不建议常规生产开启,会增加首屏体积且无法热更新,仅用于内网演示服务器。
九、常见坑位排查清单
| 现象 | 原因 | 解决 |
|---|---|---|
| mock 不生效,请求 404 / 直连后端 | Axios baseURL是绝对路径 |
mock 模式 baseURL='' |
| mock 不生效,Vite 日志无 Loaded mock | mockPath配错或文件未 export default [] |
确认 TS 文件 as MockMethod[]且 default 导出数组 |
| query 参数取不到 | handler 里要从 { query, req }解构 |
req.params取路径参数,query取 ?a=1 |
| 修改 mock 文件不热更新 | watchFiles: false或文件被 ignore |
确认 watchFiles: true,不被 .gitignore忽略 |
| proxy 和 mock 冲突 | /api被 Vite proxy 先匹配 |
mock 开启时注释掉对应 proxy,或确保 mock url 先注册 |
十、一句话总结
vite-plugin-mock 最佳实践 = 按模块拆分 + 与 Axios baseURL 解耦 + 环境变量控制开关 + 复用 Domain 类型 + Mock.js 生成数据 + 内存 DB 模拟 CRUD