vue项目中api的配置使用 - 完整解决方案与实战教程
在 Vue 项目开发中,API 配置与使用是绕不开的核心环节。无论是中小型后台管理系统,还是大型电商前台,只要涉及前后端分离,就必然要处理请求封装、环境区分、拦截器、错误统一处理等问题。很多开发者初期直接在组件里写 axios.get('/api/user'),随着项目膨胀,接口地址散落各处、环境切换靠手动改代码、token 过期无法统一跳转、重复请求导致数据错乱——这些看似零散的问题,最终会演变成维护噩梦,甚至引发线上事故。本文从真实排坑角度出发,带你彻底理清 Vue 项目中 API 的配置与使用。
【问题现象】
先看几个典型“症状”,如果你中了任意一条,说明 API 层已经失控:
- 开发环境用
localhost:3000,测试环境用192.168.x.x,每次打包前手动全局替换,漏改一次就白屏。 - 组件内直接调用 axios,没有统一拦截器,后端返回 401 时页面卡死,用户不知道要重新登录。
- 同一个接口在多个组件重复定义,改一个字段要翻遍整个
src目录。 - 请求失败没有统一提示,每个组件各写一套
catch,代码冗余且提示风格不一致。 - 并发请求时 loading 状态混乱,或者 token 过期后多个请求同时触发刷新逻辑,导致死循环。
【原因分析】
这些现象背后,本质是三个层面的缺失:
- 环境配置未抽象:Vue CLI 或 Vite 都支持
.env文件,但很多项目只用了process.env.NODE_ENV,没有定义VUE_APP_BASE_API或VITE_API_URL,导致 baseURL 硬编码。 - 请求层未封装:直接使用 axios 实例,没有创建独立的 request 模块,拦截器、错误码映射、token 注入全部缺失。
- API 管理未模块化:接口按组件散落,没有按业务域拆分
api/user.js、api/order.js,复用性极差。
【解决方案(附完整代码)】
下面以 Vue 3 + Vite + axios 为例,给出一套可直接落地的方案。Vue 2 + Vue CLI 同理,仅环境变量前缀不同。
第一步:环境变量配置
在项目根目录创建三个文件:
.env.development:开发环境.env.production:生产环境.env.test:测试环境(可选)
# .env.development
VITE_API_BASE_URL = /dev-api
VITE_APP_TITLE = 管理后台-开发
# .env.production
VITE_API_BASE_URL = https://api.yourdomain.com
VITE_APP_TITLE = 管理后台
# .env.test
VITE_API_BASE_URL = https://test-api.yourdomain.com
VITE_APP_TITLE = 管理后台-测试
注意:Vite 中只有以 VITE_ 开头的变量才会暴露给客户端。Vue CLI 则使用 VUE_APP_ 前缀。
第二步:封装 axios 请求实例
创建 src/utils/request.js,这是整个 API 层的核心:
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
import router from '@/router'
// 1. 创建 axios 实例
const service = axios.create({
// 从环境变量读取 baseURL,避免硬编码
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000 // 超时时间 15s
})
// 2. 请求拦截器:注入 token、处理重复请求
const pendingMap = new Map() // 用于取消重复请求
function getPendingKey(config) {
const { method, url, params, data } = config
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
}
service.interceptors.request.use(
config => {
// 注入 token
const token = getToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 取消重复请求(同一请求未返回前再次发起则取消前一个)
const key = getPendingKey(config)
if (pendingMap.has(key)) {
pendingMap.get(key)('取消重复请求')
pendingMap.delete(key)
}
config.cancelToken = new axios.CancelToken(c => {
pendingMap.set(key, c)
})
return config
},
error => Promise.reject(error)
)
// 3. 响应拦截器:统一处理业务码、401、错误提示
service.interceptors.response.use(
response => {
const res = response.data
// 假设后端约定 code === 200 为成功
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
// 401: token 过期或未登录
if (res.code === 401) {
ElMessageBox.confirm('登录状态已过期,请重新登录', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
removeToken()
router.push('/login')
})
}
return Promise.reject(new Error(res.message || 'Error'))
}
return res.data // 直接返回业务数据,组件无需再解构
},
error => {
// 网络层错误处理
if (axios.isCancel(error)) {
console.warn('请求被取消:', error.message)
return Promise.reject(error)
}
let message = error.message
if (error.response) {
const status = error.response.status
const statusMap = {
400: '请求参数错误',
401: '未授权,请重新登录',
403: '拒绝访问',
404: '请求地址不存在',
500: '服务器内部错误',
502: '网关错误',
503: '服务不可用'
}
message = statusMap[status] || `连接错误${status}`
} else if (message.includes('timeout')) {
message = '请求超时,请稍后重试'
} else if (message.includes('Network')) {
message = '网络异常,请检查网络连接'
}
ElMessage.error(message)
return Promise.reject(error)
}
)
export default service
第三步:按业务域组织 API 模块
创建 src/api/user.js 和 src/api/order.js,每个模块只负责定义接口,不处理逻辑:
// src/api/user.js
import request from '@/utils/request'
// 登录
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data
})
}
// 获取用户信息
export function getUserInfo(params) {
return request({
url: '/user/info',
method: 'get',
params
})
}
// src/api/order.js
import request from '@/utils/request'
export function getOrderList(params) {
return request({
url: '/order/list',
method: 'get',
params
})
}
export function createOrder(data) {
return request({
url: '/order/create',
method: 'post',
data
})
}
第四步:组件中调用
<script setup>
import { ref, onMounted } from 'vue'
import { getUserInfo } from '@/api/user'
import { getOrderList } from '@/api/order'
const userInfo = ref({})
const orderList = ref([])
onMounted(async () => {
// 由于响应拦截器已返回 res.data,这里直接拿到业务数据
userInfo.value = await getUserInfo({ id: 1 })
orderList.value = await getOrderList({ page: 1, size: 10 })
})
</script>
第五步:排查与验证步骤
配置完成后,按以下顺序验证,可快速定位问题:
- 检查
.env.development中VITE_API_BASE_URL是否生效:在vite.config.js中打印console.log(import.meta.env)或直接在 request.js 中打印 baseURL。 - 确认代理是否配置:开发环境如果使用
/dev-api前缀,必须在vite.config.js中配置server.proxy,否则请求会 404。 - 检查 token 注入:在浏览器 Network 面板查看请求头是否携带
Authorization。 - 验证 401 跳转:手动清除 localStorage 中的 token,发起请求,观察是否弹出重新登录确认框。
- 验证重复请求取消:快速点击同一按钮两次,Network 中应只保留一个请求,另一个显示
canceled。
最后补充一个 Vite 代理配置示例,避免开发环境跨域:
// vite.config.js
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: [vue()],
server: {
proxy: {
'/dev-api': {
target: 'http://localhost:8080', // 后端真实地址
changeOrigin: true,
rewrite: path => path.replace(/^\/dev-api/, '')
}
}
}
}
})
这套方案的核心思想是:环境变量管地址,request 实例管行为,api 模块管定义,组件只管调用。四层分离后,无论项目扩展到多少接口,API 层始终清晰可控,线上事故率会大幅下降。