锦书在线
80.52M · 2026-03-21
在前端开发中,网络请求是连接前后端的桥梁,但也常常成为开发效率的瓶颈。跨域问题、后端接口未就绪、环境不稳定,这些问题每天都在消耗着我们的时间和精力。我们可以先看几个场景:
我们正在开发一个新功能,需要调用 /api/user/login 接口;启动项目,点击登录,浏览器报错:
Access to fetch at 'http://localhost:3000/api/user/login'
from origin 'http://localhost:5173' has been blocked by CORS policy
然后我们去找后端同事:"帮我配一下CORS"。后端说:"好的,等我5分钟"。那我们就只能干等着。
我们要开发一个复杂报表页面,需要调用 /api/report/complex-data。但后端说这个接口要下周才能好;我们只能先写静态数据,等接口好了再改代码联调。
我们要测试页面在接口返回 500 错误时的表现,但后端服务表现得一直很稳定,怎么也触发不了错误。
这些问题每天都在消耗着我们的时间和精力。而Vite提供的代理和Mock能力,正是解决这些痛点的利器。
我们可以用一个快递的例子,来理解什么是代理: 我们(浏览器)想通过公司内部快递,寄一份快递给后端,但快递公司说"不同地址不能寄"(跨域):
这个例子的关键是:综合员和你是同一个部门(地址),所以快递公司不拦截。
请求流程:
浏览器 →
↓
Vite 开发服务器 (localhost:5173)
↓
代理配置匹配 /api
↓
转发请求 →
↓
后端服务器 (localhost:3000)
↓
响应返回 → Vite 服务器
↓
转发给浏览器
关键:浏览器只和同源的 Vite 服务器通信,完美绕过跨域
// vite.config.js
export default {
server: {
proxy: {
// 把所有 /api 开头的请求,转发到
'/api': 'http://localhost:3000'
}
}
}
// 现在可以这样请求了
fetch('/api/users')
// 实际请求:
// 完美绕过跨域!
// vite.config.js
export default {
server: {
proxy: {
// 详细的代理配置
'/api': {
target: 'http://localhost:3000', // 目标服务器地址
changeOrigin: true, // 改变请求源头(重要!)
// 重写路径:去掉 /api 前缀
rewrite: (path) => path.replace(/^/api/, ''),
// 请求 /api/users → 实际转发 /users
secure: false, // 如果目标是https但证书无效,设为false
ws: true, // 支持 WebSocket 代理
// 添加自定义请求头
headers: {
'X-Dev-Proxy': 'vite'
},
// 调试:查看代理过程
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq, req) => {
console.log('→ 代理请求:', req.url)
})
proxy.on('proxyRes', (proxyRes, req) => {
console.log('← 代理响应:', proxyRes.statusCode)
})
proxy.on('error', (err) => {
console.log(' 代理错误:', err)
})
}
}
}
}
}
实际开发中,我们通常需要配置多个环境:
每次切换环境都要改代码,这太麻烦了!
// vite.config.js
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ mode }) => {
// 根据当前模式加载对应的环境变量
// mode 可能是 development / staging / production
const env = loadEnv(mode, process.cwd())
return {
server: {
proxy: {
'/api': {
target: env.VITE_API_URL, // 从环境变量读取
changeOrigin: true,
rewrite: path => path.replace(/^/api/, '')
},
// 如果有多个后端服务
'/upload': {
target: env.VITE_UPLOAD_URL,
changeOrigin: true
}
}
}
}
})
# .env.development - 开发环境
VITE_API_URL=
VITE_UPLOAD_URL=
# .env.staging - 测试环境
VITE_API_URL=
VITE_UPLOAD_URL=
# .env.production - 生产环境
VITE_API_URL=
VITE_UPLOAD_URL=
// package.json
{
"scripts": {
"dev": "vite --mode development",
"dev:staging": "vite --mode staging",
"build:prod": "vite build --mode production"
}
}
真实接口后端需要开发 2 周后才能完成;此时前端不能等,需要继续开发,我们就可以 Mock 数据继续开发:
// 解决方案:Mock 数据
fetch('/api/complex-report')
.then(res => res.json())
.then(data => renderReport(data)) // 用 Mock 数据继续开发
const testCases = [
{ status: 200, data: [...] }, // 正常情况
{ status: 500, message: '服务器错误' }, // 错误情况
{ status: 401, message: '未登录' }, // 权限问题
{ status: 200, data: [] } // 空数据情况
]
不需要真实后端,通过 Mock 数据,前端也能正常跑起来!
npm install vite-plugin-mock -D
// vite.config.js
import { viteMockServe } from 'vite-plugin-mock'
export default {
plugins: [
viteMockServe({
mockPath: 'mock', // mock文件存放目录
supportTs: true, // 支持TypeScript
watchFiles: true, // 文件变化(修改mock自动重启)
localEnabled: true, // 开发环境启用
prodEnabled: false, // 生产环境禁用
// 生产环境注入的代码(如果需要)
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`
})
]
}
// mock/user.js
export default [
// GET请求示例
{
url: '/api/users',
method: 'get',
response: () => {
return {
code: 200,
message: 'success',
data: [
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 }
]
}
}
},
// POST请求示例
{
url: '/api/login',
method: 'post',
response: ({ body }) => {
const { username, password } = body
// 模拟登录验证
if (username === 'admin' && password === '123456') {
return {
code: 200,
data: {
token: 'mock-token-' + Date.now(),
username
}
}
}
return {
code: 401,
message: '用户名或密码错误'
}
}
}
]
// mock/user.js
export default [
// 动态路径参数
{
url: '/api/user/:id',
method: 'get',
response: ({ params }) => {
const { id } = params
return {
code: 200,
data: {
id: Number(id),
name: `用户${id}`,
age: 20 + Number(id),
avatar: `${id % 2 ? 'men' : 'women'}/${id}.jpg`
}
}
}
},
// 查询参数
{
url: '/api/users',
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query
// 生成分页数据
const start = (page - 1) * pageSize
const total = 100
const data = Array.from({ length: pageSize }, (_, i) => ({
id: start + i + 1,
name: `用户${start + i + 1}`,
age: 20 + Math.floor(Math.random() * 30)
}))
return {
code: 200,
data: {
list: data,
total,
page: Number(page),
pageSize: Number(pageSize)
}
}
}
}
]
// mock/scenarios.ts
export default [
// 模拟分页数据
{
url: '/api/users/paged',
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query
const start = (page - 1) * pageSize
const total = 100
const data = Array.from({ length: pageSize }, (_, i) => ({
id: start + i + 1,
name: `用户${start + i + 1}`,
age: 20 + Math.floor(Math.random() * 30)
}))
return {
code: 200,
data: {
list: data,
total,
page: Number(page),
pageSize: Number(pageSize)
}
}
}
},
// 模拟延迟
{
url: '/api/slow-request',
method: 'get',
timeout: 3000, // 3秒延迟
response: () => {
return {
code: 200,
data: '终于响应了'
}
}
},
// 模拟错误
{
url: '/api/error',
method: 'get',
statusCode: 500,
response: () => {
return {
code: 500,
message: '服务器内部错误'
}
}
},
// 模拟超时
{
url: '/api/timeout',
method: 'get',
timeout: 10000, // 超时时间
response: () => {
// 永远不会执行
}
}
]
// mock/dashboard.js
import Mock from 'mockjs'
export default [
{
url: '/api/dashboard',
method: 'get',
response: () => {
return {
code: 200,
data: {
// 随机数字
visits: Mock.mock('@integer(1000, 10000)'),
// 随机浮点数
sales: Mock.mock('@float(1000, 10000, 2, 2)'),
// 随机数组
trends: Mock.mock({
'data|7': ['@integer(100, 1000)']
}).data,
// 随机用户列表
users: Mock.mock({
'list|10': [{
'id|+1': 1,
name: '@cname', // 中文名
avatar: '@image(100x100)', // 随机图片
'age|20-40': 1,
email: '@email',
address: '@county(true)',
'gender|1': ['男', '女']
}]
}).list
}
}
}
}
]
// mock/crud.js
// 模拟数据库
const store = {
users: new Map([
[1, { id: 1, name: '张三' }],
[2, { id: 2, name: '李四' }]
])
}
export default [
// 查询列表
{
url: '/api/users',
method: 'get',
response: () => ({
code: 200,
data: Array.from(store.users.values())
})
},
// 新增
{
url: '/api/users',
method: 'post',
response: ({ body }) => {
const id = store.users.size + 1
const newUser = { id, ...body }
store.users.set(id, newUser)
return {
code: 200,
data: newUser
}
}
},
// 删除
{
url: '/api/users/:id',
method: 'delete',
response: ({ params }) => {
const id = Number(params.id)
const deleted = store.users.get(id)
store.users.delete(id)
return {
code: 200,
data: deleted
}
}
},
// 更新
{
url: '/api/users/:id',
method: 'put',
response: ({ params, body }) => {
const id = Number(params.id)
const user = store.users.get(id)
if (user) {
const updated = { ...user, ...body }
store.users.set(id, updated)
return {
code: 200,
data: updated
}
}
return {
code: 404,
message: '用户不存在'
}
}
}
]
// vite.config.js
import { defineConfig, loadEnv } from 'vite'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
// 是否启用Mock(从环境变量读取)
const useMock = env.VITE_USE_MOCK === 'true'
return {
server: {
proxy: {
'/api': {
target: env.VITE_API_URL,
changeOrigin: true
}
}
},
plugins: [
// 只有启用Mock时才加载插件
useMock && viteMockServe({
mockPath: 'mock',
localEnabled: true
})
].filter(Boolean)
}
})
# .env.development - 正常开发(连接真实后端)
VITE_API_URL=
VITE_USE_MOCK=false
# .env.development.mock - Mock模式(不依赖后端)
VITE_API_URL= # 这个其实用不到了
VITE_USE_MOCK=true
# .env.staging - 测试环境
VITE_API_URL=
VITE_USE_MOCK=false
{
"scripts": {
"dev": "vite --mode development",
"dev:mock": "vite --mode development.mock",
"dev:user": "VITE_USE_MOCK=true vite", // 临时启用Mock
"dev:no-mock": "VITE_USE_MOCK=false vite" // 临时关闭Mock
}
}
// src/utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_URL
})
// 请求拦截器 - 可以添加统一处理
request.interceptors.request.use(config => {
// 添加token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器 - 统一处理错误
request.interceptors.response.use(
response => response.data,
error => {
// 统一错误处理
if (error.response?.status === 401) {
// 跳转登录
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
project/
├── mock/
│ ├── index.ts # 主入口,导出所有 Mock
│ ├── utils/
│ │ ├── response.ts # 响应工具函数
│ │ ├── database.ts # 模拟数据库
│ │ └── generator.ts # 数据生成器
│ ├── modules/
│ │ ├── user/
│ │ │ ├── index.ts # 用户模块 Mock
│ │ │ ├── data.ts # 用户数据
│ │ │ └── scenarios.ts # 用户场景
│ │ ├── order/
│ │ │ ├── index.ts
│ │ │ ├── data.ts
│ │ │ └── scenarios.ts
│ │ └── product/
│ │ ├── index.ts
│ │ ├── data.ts
│ │ └── scenarios.ts
│ └── fixtures/
│ ├── users.json # 静态数据
│ └── products.json
└── vite.config.ts
// mock/utils/response.ts
export interface ApiResponse<T = any> {
code: number
message: string
data: T
}
// 成功响应
export const success = <T>(data: T, message = 'success'): ApiResponse<T> => ({
code: 200,
message,
data
})
// 错误响应
export const error = (message: string, code = 500): ApiResponse => ({
code,
message,
data: null
})
// 分页响应
export const paged = <T>(
list: T[],
total: number,
page: number,
pageSize: number
) => success({
list,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize)
})
// mock/index.js
import user from './modules/user'
import order from './modules/order'
import product from './modules/product'
// 合并所有mock
export default [
...user,
...order,
...product
]
// mock/modules/user.js
import { success, error } from '../utils/response'
export default [
// 登录
{
url: '/api/login',
method: 'post',
response: ({ body }) => {
const { username, password } = body
if (username === 'admin' && password === '123456') {
return success({
token: 'mock-token',
username
})
}
return error('用户名或密码错误', 401)
}
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: ({ headers }) => {
const token = headers.authorization
if (!token) {
return error('未登录', 401)
}
return success({
id: 1,
name: '张三',
avatar: 'https://randomuser.me/api/portraits/men/1.jpg',
roles: ['admin']
})
}
}
]
fetch('/api/users') // 正确
fetch('api/users') // 错误,缺少斜杠
proxy: {
'/api': 'http://localhost:3000' // 请求会转发到
// 如果需要重写路径:
'/api': {
target: 'http://localhost:3000',
rewrite: path => path.replace(/^/api/, '') // 转发到
}
}
curl
// vite.config.ts
export default {
plugins: [
viteMockServe({
watchFiles: true, // 确保开启
// 或者手动清除缓存
logger: true // 查看日志
})
]
}
// 如果还是不更新,尝试:
// 1. 重启开发服务器
// 2. 删除 node_modules/.vite 缓存
// 3. 检查文件修改时间
场景:同一个路径既配置了代理,又配置了 Mock,这时可能会引发冲突
plugins: [
viteMockServe({
// 确保 Mock 插件在代理之前
// 插件的顺序决定了优先级
})
]
proxy: {
'/api/real': 'http://localhost:3000', // 真实 API
}
// Mock 使用相同路径,但通过插件配置
const useMock = process.env.USE_MOCK === 'true'
proxy: {
...(!useMock && {
'/api': 'http://localhost:3000'
})
}
// 请求封装中判断
const baseURL = import.meta.env.PROD
? 'https://api.example.com' // 生产用真实地址
: '/api' // 开发用代理
const request = axios.create({ baseURL })
阶段1:后端接口未定义
├─ 前端先定义接口格式
├─ 编写Mock数据
└─ 前端独立开发
阶段2:后端开发中
├─ 已完成的接口用代理
├─ 未完成的用Mock
└─ 逐步替换
阶段3:后端全部完成
├─ 关闭Mock
├─ 全部使用代理
└─ 联调测试
阶段4:特殊场景测试
├─ 临时启用Mock
├─ 模拟各种边界情况
└─ 测试完成后关闭
// vite.config.js - 完整配置模板
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const useMock = env.VITE_USE_MOCK === 'true'
return {
plugins: [
vue(),
useMock && viteMockServe({
mockPath: 'mock',
supportTs: true,
watchFiles: true,
logger: true
})
].filter(Boolean),
server: {
proxy: {
'/api': {
target: env.VITE_API_URL,
changeOrigin: true,
rewrite: path => path.replace(/^/api/, ''),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq, req) => {
console.log('[代理]', req.method, req.url)
})
}
}
}
}
}
})
代理和Mock不是用来骗人的,而是用来解放前端的。好的代理和Mock策略应该是:
掌握了这些,我们就可以告别跨域报错,告别等待后端,让开发效率真正起飞!
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!