前言
在前端开发中,网络请求是连接前后端的桥梁,但也常常成为开发效率的瓶颈。跨域问题、后端接口未就绪、环境不稳定,这些问题每天都在消耗着我们的时间和精力。我们可以先看几个场景:
场景1
我们正在开发一个新功能,需要调用 /api/user/login 接口;启动项目,点击登录,浏览器报错:
bash
Access to fetch at 'http://localhost:3000/api/user/login'
from origin 'http://localhost:5173' has been blocked by CORS policy
然后我们去找后端同事:"帮我配一下CORS"。后端说:"好的,等我5分钟"。那我们就只能干等着。
场景2
我们要开发一个复杂报表页面,需要调用 /api/report/complex-data。但后端说这个接口要下周才能好;我们只能先写静态数据,等接口好了再改代码联调。
场景3
我们要测试页面在接口返回 500 错误时的表现,但后端服务表现得一直很稳定,怎么也触发不了错误。
这些问题每天都在消耗着我们的时间和精力。而Vite提供的代理和Mock能力,正是解决这些痛点的利器。
为什么要在Vite层解决网络请求问题?
开发环境的三大网络困境
困境1:跨域问题
- 前端在 localhost:5173
- 后端在 localhost:3000
- 浏览器:不同端口 → 跨域 → 拦截
困境2:接口未就绪
- 后端说要下周才能好
- 前端这周只能干等?
困境3:环境不稳定
- 测试服务器时而500,时而超时
- 开发效率直线下降
传统方案的问题
- 跨域:让后端配CORS → 依赖后端,每次新增接口都要配
- Mock:单独启动一个Mock服务 → 多维护一个服务,端口冲突
- 环境问题:手动改代码 → 容易忘记改回来,导致生产事故
Vite方案的优势
- 代理:开发服务器自动转发 → 零后端依赖,前端完全可控
- Mock:插件注入拦截 → 无额外服务,随项目启动
- 配置中心化 → 一键切换,不会污染业务代码
代理 - 优雅解决跨域问题
代理是什么?
我们可以用一个快递的例子,来理解什么是代理: 我们(浏览器)想通过公司内部快递,寄一份快递给后端,但快递公司说"不同地址不能寄"(跨域):
- 于是我们找了个中间人:公司综合员(Vite开发服务器)
- 把快递给综合员(请求发给Vite)
- 综合员帮我们转寄给后端(Vite转发请求)
- 后端把回执给综合员,综合员再转交给我们
这个例子的关键是:综合员和你是同一个部门(地址),所以快递公司不拦截。
Vite 代理的工作原理
bash
请求流程:
浏览器 → http://localhost:5173/api/users
↓
Vite 开发服务器 (localhost:5173)
↓
代理配置匹配 /api
↓
转发请求 → http://localhost:3000/api/users
↓
后端服务器 (localhost:3000)
↓
响应返回 → Vite 服务器
↓
转发给浏览器
关键:浏览器只和同源的 Vite 服务器通信,完美绕过跨域
最简单的代理配置
javascript
// vite.config.js
export default {
server: {
proxy: {
// 把所有 /api 开头的请求,转发到 http://localhost:3000
'/api': 'http://localhost:3000'
}
}
}
// 现在可以这样请求了
fetch('/api/users')
// 实际请求:http://localhost:3000/api/users
// 完美绕过跨域!
完整的代理配置详解
javascript
// 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)
})
}
}
}
}
}
多环境代理配置策略
为什么需要多环境?
实际开发中,我们通常需要配置多个环境:
- 开发环境:连接本地后端 localhost:3000
- 测试环境:连接测试服务器 test-api.example.com
- 预发环境:连接预发服务器 staging-api.example.com
- 生产环境:连接正式服务器 api.example.com
每次切换环境都要改代码,这太麻烦了!
使用环境变量配置
javascript
// 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
}
}
}
}
})
环境变量文件配置
bash
# .env.development - 开发环境
VITE_API_URL=http://localhost:3000
VITE_UPLOAD_URL=http://localhost:3001
# .env.staging - 测试环境
VITE_API_URL=http://test-api.example.com
VITE_UPLOAD_URL=http://test-upload.example.com
# .env.production - 生产环境
VITE_API_URL=https://api.example.com
VITE_UPLOAD_URL=https://upload.example.com
启动不同环境
json
// package.json
{
"scripts": {
"dev": "vite --mode development",
"dev:staging": "vite --mode staging",
"build:prod": "vite build --mode production"
}
}
Mock - 摆脱后端依赖
什么时候需要Mock?
场景1:后端接口还没开发完成
真实接口后端需要开发 2 周后才能完成;此时前端不能等,需要继续开发,我们就可以 Mock 数据继续开发:
javascript
// 解决方案:Mock 数据
fetch('/api/complex-report')
.then(res => res.json())
.then(data => renderReport(data)) // 用 Mock 数据继续开发
场景2:测试边界情况
javascript
const testCases = [
{ status: 200, data: [...] }, // 正常情况
{ status: 500, message: '服务器错误' }, // 错误情况
{ status: 401, message: '未登录' }, // 权限问题
{ status: 200, data: [] } // 空数据情况
]
场景3:演示或测试环境
不需要真实后端,通过 Mock 数据,前端也能正常跑起来!
安装 vite-plugin-mock
bash
npm install vite-plugin-mock -D
基础配置
javascript
// 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接口
javascript
// 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
javascript
// 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: `https://randomuser.me/api/portraits/${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技巧
模拟不同场景
typescript
// 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: () => {
// 永远不会执行
}
}
]
使用 mockjs 生成随机数据
javascript
// 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
}
}
}
}
]
动态增删改查
javascript
// 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: '用户不存在'
}
}
}
]
代理与Mock协同工作
按需启用Mock
javascript
// 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)
}
})
环境变量配置
bash
# .env.development - 正常开发(连接真实后端)
VITE_API_URL=http://localhost:3000
VITE_USE_MOCK=false
# .env.development.mock - Mock模式(不依赖后端)
VITE_API_URL=http://localhost:3000 # 这个其实用不到了
VITE_USE_MOCK=true
# .env.staging - 测试环境
VITE_API_URL=http://test-api.example.com
VITE_USE_MOCK=false
启动脚本配置
json
{
"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
}
}
请求封装配合
javascript
// 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
最佳实践与项目组织
Mock 文件组织结构
bash
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
统一响应格式
typescript
// 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)
})
主入口文件
javascript
// mock/index.js
import user from './modules/user'
import order from './modules/order'
import product from './modules/product'
// 合并所有mock
export default [
...user,
...order,
...product
]
模块化示例
javascript
// 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']
})
}
}
]
常见问题与解决方案
问题一:代理不生效
检查点1:路径是否正确
javascript
fetch('/api/users') // 正确
fetch('api/users') // 错误,缺少斜杠
检查点2:代理配置是否正确
javascript
proxy: {
'/api': 'http://localhost:3000' // 请求会转发到 http://localhost:3000/api
// 如果需要重写路径:
'/api': {
target: 'http://localhost:3000',
rewrite: path => path.replace(/^\/api/, '') // 转发到 http://localhost:3000
}
}
检查点3:后端是否在运行
bash
curl http://localhost:3000/api/test
问题二:Mock 数据不更新
typescript
// vite.config.ts
export default {
plugins: [
viteMockServe({
watchFiles: true, // 确保开启监听
// 或者手动清除缓存
logger: true // 查看日志
})
]
}
// 如果还是不更新,尝试:
// 1. 重启开发服务器
// 2. 删除 node_modules/.vite 缓存
// 3. 检查文件修改时间
问题三:代理和 Mock 冲突
场景:同一个路径既配置了代理,又配置了 Mock,这时可能会引发冲突
解决方案1:优先级控制
javascript
plugins: [
viteMockServe({
// 确保 Mock 插件在代理之前
// 插件的顺序决定了优先级
})
]
解决方案2:使用不同路径
javascript
proxy: {
'/api/real': 'http://localhost:3000', // 真实 API
}
// Mock 使用相同路径,但通过插件配置
解决方案3:条件启用
javascript
const useMock = process.env.USE_MOCK === 'true'
proxy: {
...(!useMock && {
'/api': 'http://localhost:3000'
})
}
问题四:开发环境正常,生产环境报404
解决方案:确保生产环境用真实接口
javascript
// 请求封装中判断
const baseURL = import.meta.env.PROD
? 'https://api.example.com' // 生产用真实地址
: '/api' // 开发用代理
const request = axios.create({ baseURL })
代理与 Mock 的最佳实践
配置清单
- 基础代理配置完成
- 多环境代理配置
- Mock 插件安装配置
- Mock 接口编写规范
- 环境变量控制开关
- 代理与 Mock 协同策略
开发流程建议
阶段1:后端接口未定义
├─ 前端先定义接口格式
├─ 编写Mock数据
└─ 前端独立开发
阶段2:后端开发中
├─ 已完成的接口用代理
├─ 未完成的用Mock
└─ 逐步替换
阶段3:后端全部完成
├─ 关闭Mock
├─ 全部使用代理
└─ 联调测试
阶段4:特殊场景测试
├─ 临时启用Mock
├─ 模拟各种边界情况
└─ 测试完成后关闭
常用配置模板
javascript
// 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策略应该是:
- 开发时:前端不依赖后端,想怎么测就怎么测
- 联调时:一键关闭Mock,无缝切换到真实接口
- 维护时:配置清晰,不会因为忘记关Mock而出问题
掌握了这些,我们就可以告别跨域报错,告别等待后端,让开发效率真正起飞!
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!