网络请求在Vite层的代理与Mock:告别跨域和后端依赖

前言

在前端开发中,网络请求是连接前后端的桥梁,但也常常成为开发效率的瓶颈。跨域问题、后端接口未就绪、环境不稳定,这些问题每天都在消耗着我们的时间和精力。我们可以先看几个场景:

场景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)
          })
        }
      }
    }
  }
}

多环境代理配置策略

为什么需要多环境?

实际开发中,我们通常需要配置多个环境:

每次切换环境都要改代码,这太麻烦了!

使用环境变量配置

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)
            })
          }
        }
      }
    }
  }
})

三个黄金原则

  1. 需要时才启用,不需要时关闭
  2. 模拟真实场景,不止是成功路径
  3. 与代理无缝切换,对业务代码无侵入

结语

代理和Mock不是用来骗人的,而是用来解放前端的。好的代理和Mock策略应该是:

  • 开发时:前端不依赖后端,想怎么测就怎么测
  • 联调时:一键关闭Mock,无缝切换到真实接口
  • 维护时:配置清晰,不会因为忘记关Mock而出问题

掌握了这些,我们就可以告别跨域报错,告别等待后端,让开发效率真正起飞!

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
小彭努力中2 小时前
193.Vue3 + OpenLayers 实战:圆孔相机模型推算卫星拍摄区域
vue.js·数码相机·vue·openlayers·geojson
用户69371750013846 小时前
Google 正在“收紧侧加载”:陌生 APK 安装或需等待 24 小时
android·前端
蓝帆傲亦7 小时前
Web 前端搜索文字高亮实现方法汇总
前端
用户69371750013847 小时前
Room 3.0:这次不是升级,是重来
android·前端·google
漫随流水8 小时前
旅游推荐系统(view.py)
前端·数据库·python·旅游
踩着两条虫9 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
jzlhll12310 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
用头发抵命10 小时前
Vue 3 中优雅地集成 Video.js 播放器:从组件封装到功能定制
开发语言·javascript·ecmascript
蓝冰凌11 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js