API 集成与请求处理
本文档详细介绍如何在 Antdv Next Admin 项目中集成后端 API,包括 Axios 配置、请求拦截器、错误处理、认证流程等。
目录
HTTP 客户端配置
项目的 HTTP 客户端配置位于 src/utils/request.ts。
基础配置
typescript
import axios from 'axios'
const request = axios.create({
// API 基础路径
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
// 请求超时时间(毫秒)
timeout: 10000,
// 请求头
headers: {
'Content-Type': 'application/json',
},
})环境变量配置
bash
# .env.development(开发环境)
VITE_API_BASE_URL=/api
VITE_USE_MOCK=true
# .env.production(生产环境)
VITE_API_BASE_URL=https://api.your-domain.com
VITE_USE_MOCK=false请求拦截器
请求拦截器用于在请求发送前统一处理,如添加 Token、设置请求头等。
添加认证 Token
typescript
request.interceptors.request.use(
(config) => {
// 从 localStorage 获取 Token
const token = localStorage.getItem('access_token')
// 如果有 Token,添加到请求头
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加时间戳防止缓存
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now(),
}
}
return config
},
(error) => {
return Promise.reject(error)
}
)请求加载状态
typescript
import { message } from 'antdv-next'
let requestCount = 0
const showLoading = () => {
requestCount++
// 显示全局加载状态
}
const hideLoading = () => {
requestCount--
if (requestCount <= 0) {
// 隐藏全局加载状态
}
}
request.interceptors.request.use(
(config) => {
// 非静默请求显示加载
if (!config.silent) {
showLoading()
}
return config
}
)
request.interceptors.response.use(
(response) => {
if (!response.config.silent) {
hideLoading()
}
return response
},
(error) => {
if (!error.config?.silent) {
hideLoading()
}
return Promise.reject(error)
}
)自定义配置选项
typescript
// 静默请求(不显示加载状态)
request.get('/api/config', { silent: true })
// 自定义超时
request.get('/api/large-data', { timeout: 30000 })
// 自定义请求头
request.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})响应拦截器
响应拦截器用于统一处理响应数据和错误。
标准响应格式
项目期望的后端响应格式:
typescript
interface ApiResponse<T> {
code: number // 状态码:200 表示成功
data: T // 响应数据
message: string // 提示信息
}响应拦截器实现
typescript
request.interceptors.response.use(
(response) => {
const { data } = response
// 处理标准响应格式
if (data.code !== 200) {
// 业务错误
message.error(data.message || '请求失败')
return Promise.reject(new Error(data.message))
}
// 返回数据部分
return data.data
},
(error) => {
// HTTP 错误处理
return Promise.reject(error)
}
)Token 认证与刷新
双 Token 机制
项目使用 Access Token + Refresh Token 的双 Token 机制:
- Access Token:短期有效(如 15 分钟),用于 API 认证
- Refresh Token:长期有效(如 7 天),用于获取新的 Access Token
Token 刷新流程
typescript
// 是否在刷新 Token
let isRefreshing = false
// 等待刷新完成的请求队列
let refreshSubscribers: Array<(token: string) => void> = []
// 订阅 Token 刷新
const subscribeTokenRefresh = (callback: (token: string) => void) => {
refreshSubscribers.push(callback)
}
// 通知所有订阅者
const onTokenRefreshed = (token: string) => {
refreshSubscribers.forEach(callback => callback(token))
refreshSubscribers = []
}
// 响应拦截器中处理 401
request.interceptors.response.use(
(response) => response,
async (error) => {
const { response, config } = error
// Token 过期(401)且不是刷新 Token 的请求
if (response?.status === 401 && !config.url?.includes('/refresh')) {
if (!isRefreshing) {
isRefreshing = true
try {
// 调用刷新接口
const refreshToken = localStorage.getItem('refresh_token')
const { data } = await axios.post('/api/auth/refresh', {
refreshToken,
})
// 保存新 Token
const newToken = data.data.token
localStorage.setItem('access_token', newToken)
// 通知等待的请求
onTokenRefreshed(newToken)
isRefreshing = false
// 重试原请求
config.headers.Authorization = `Bearer ${newToken}`
return request(config)
} catch (refreshError) {
// 刷新失败,登出
isRefreshing = false
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
window.location.href = '/login'
return Promise.reject(refreshError)
}
}
// 正在刷新,将请求加入队列
return new Promise((resolve) => {
subscribeTokenRefresh((token) => {
config.headers.Authorization = `Bearer ${token}`
resolve(request(config))
})
})
}
return Promise.reject(error)
}
)使用 Auth Store 管理 Token
项目中已封装好 Token 管理,推荐直接使用 useAuthStore:
typescript
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
// 登录
await authStore.login(username, password)
// 登出
authStore.logout()
// Token 会自动在请求拦截器中添加错误处理
全局错误处理
typescript
import { message, notification } from 'antdv-next'
request.interceptors.response.use(
(response) => response,
(error) => {
const { response } = error
if (response) {
switch (response.status) {
case 400:
message.error(response.data?.message || '请求参数错误')
break
case 401:
// Token 过期,由上面的拦截器处理
break
case 403:
message.error('没有权限执行此操作')
break
case 404:
message.error('请求的资源不存在')
break
case 500:
notification.error({
message: '服务器错误',
description: response.data?.message || '请稍后重试',
})
break
default:
message.error(response.data?.message || '请求失败')
}
} else if (error.request) {
// 请求已发送但没有收到响应
message.error('网络错误,请检查网络连接')
} else {
// 请求配置出错
message.error('请求配置错误')
}
return Promise.reject(error)
}
)业务错误处理
在 API 调用处处理特定业务错误:
typescript
import { getUserList } from '@/api/user'
try {
const data = await getUserList(params)
} catch (error: any) {
if (error.response?.data?.code === 1001) {
// 特定业务错误码处理
message.warning('用户列表为空')
} else {
throw error // 其他错误继续抛出
}
}从 Mock 切换到真实 API
步骤 1:修改环境变量
bash
# .env.development
VITE_USE_MOCK=false
VITE_API_BASE_URL=http://localhost:8080步骤 2:移除 Mock 拦截器(如使用 MSW)
如果项目使用了 MSW 等 Mock 库,需要在 main.ts 中移除:
typescript
// main.ts
// 移除或注释掉以下代码
// if (import.meta.env.VITE_USE_MOCK === 'true') {
// import('./mock')
// }步骤 3:调整 API 响应格式
如果后端返回格式不同,修改响应拦截器:
typescript
request.interceptors.response.use(
(response) => {
const { data } = response
// 适配不同的响应格式
// 格式 A: { code: 200, data: {}, message: '' }
// 格式 B: { status: 'success', result: {} }
// 格式 C: 直接返回数据
if (data.code === 200 || data.status === 'success') {
return data.data || data.result || data
}
return Promise.reject(new Error(data.message || data.error))
}
)步骤 4:处理跨域
开发环境在 vite.config.ts 中配置代理:
typescript
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''), // 如需移除 /api 前缀
},
},
},
})请求重试
自动重试机制
typescript
import axiosRetry from 'axios-retry'
axiosRetry(request, {
retries: 3, // 重试次数
retryDelay: (retryCount) => {
return retryCount * 1000 // 延迟时间(毫秒)
},
retryCondition: (error) => {
// 只在网络错误或 5xx 错误时重试
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status >= 500
},
})手动重试
typescript
const fetchWithRetry = async (apiCall: Function, retries = 3) => {
for (let i = 0; i < retries; i++) {
try {
return await apiCall()
} catch (error) {
if (i === retries - 1) throw error
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
}
// 使用
const data = await fetchWithRetry(() => getUserList(params))文件上传下载
文件上传
typescript
// API 接口
export const uploadFile = (file: File, onProgress?: (percent: number) => void) => {
const formData = new FormData()
formData.append('file', file)
return request.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1)
)
onProgress?.(percent)
},
})
}组件中使用:
vue
<template>
<a-upload
:custom-request="handleUpload"
:show-upload-list="false"
>
<a-button>
<upload-outlined />
上传文件
</a-button>
</a-upload>
</template>
<script setup>
import { uploadFile } from '@/api/file'
const handleUpload = async ({ file, onSuccess, onError }) => {
try {
const result = await uploadFile(file, (percent) => {
console.log('上传进度:', percent + '%')
})
onSuccess(result)
} catch (error) {
onError(error)
}
}
</script>文件下载
typescript
// API 接口
export const downloadFile = (url: string, filename: string) => {
return request.get(url, {
responseType: 'blob',
}).then((blob) => {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
})
}
// 使用
downloadFile('/api/export/users', '用户列表.xlsx')最佳实践
1. API 模块化组织
按业务领域组织 API 文件:
src/api/
├── auth.ts # 认证相关
├── user.ts # 用户管理
├── role.ts # 角色权限
├── system.ts # 系统设置
└── file.ts # 文件操作2. 类型安全
为所有 API 定义请求和响应类型:
typescript
// types/api.ts
export interface ApiResponse<T> {
code: number
data: T
message: string
}
export interface User {
id: string
username: string
email: string
}
// api/user.ts
export const getUserList = (params: PaginationParams) => {
return request.get<ApiResponse<PaginatedResult<User>>>('/api/users', { params })
}3. 取消请求
typescript
import axios from 'axios'
const controller = new AbortController()
request.get('/api/large-data', {
signal: controller.signal,
})
// 取消请求
controller.abort()4. 并发控制
typescript
import { throttle, debounce } from 'lodash-es'
// 节流:限制请求频率
const throttledSearch = throttle((keyword) => {
return searchApi(keyword)
}, 300)
// 防抖:等待输入停止
const debouncedSave = debounce((data) => {
return saveApi(data)
}, 500)