request.js是基于axios封装的HTTP请求工具,提供统一的请求配置、拦截器处理、错误处理等功能。
文件位置: src/utils/request.js
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/stores/user'
import router from '@/router'
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求配置
const requestConfig = {
// 基础URL
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
// 超时时间
timeout: 10000,
// 是否携带凭证
withCredentials: true,
// 请求头
headers: {
'Content-Type': 'application/json;charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
},
// 响应类型
responseType: 'json',
// 最大重定向次数
maxRedirects: 5,
// 最大内容长度
maxContentLength: 2000,
// 最大请求体长度
maxBodyLength: 2000
}
// 应用配置
Object.assign(request.defaults, requestConfig)// 请求拦截器
request.interceptors.request.use(
(config) => {
// 添加时间戳防止缓存
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now()
}
}
// 添加认证Token
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
// 添加请求ID用于追踪
config.headers['X-Request-ID'] = generateRequestId()
// 添加语言标识
config.headers['Accept-Language'] = getLanguage()
// 请求开始时间
config.startTime = Date.now()
// 显示加载状态
if (config.showLoading !== false) {
showLoading()
}
// 请求去重处理
const requestKey = generateRequestKey(config)
if (pendingRequests.has(requestKey)) {
const cancelToken = pendingRequests.get(requestKey)
cancelToken.cancel('重复请求')
}
// 创建取消令牌
const source = axios.CancelToken.source()
config.cancelToken = source.token
pendingRequests.set(requestKey, source)
return config
},
(error) => {
hideLoading()
console.error('请求拦截器错误:', error)
return Promise.reject(error)
}
)// 响应拦截器
request.interceptors.response.use(
(response) => {
const config = response.config
// 计算请求耗时
const duration = Date.now() - config.startTime
console.log(`API请求耗时: ${config.url} - ${duration}ms`)
// 隐藏加载状态
if (config.showLoading !== false) {
hideLoading()
}
// 移除待处理请求
const requestKey = generateRequestKey(config)
pendingRequests.delete(requestKey)
// 处理响应数据
const { data } = response
// 统一响应格式处理
if (data && typeof data === 'object') {
// 检查业务状态码
if (data.status === false) {
handleBusinessError(data, config)
return Promise.reject(new Error(data.msg || '请求失败'))
}
// 返回数据
return data
}
// 直接返回响应数据
return data
},
(error) => {
const config = error.config
// 隐藏加载状态
if (config && config.showLoading !== false) {
hideLoading()
}
// 移除待处理请求
if (config) {
const requestKey = generateRequestKey(config)
pendingRequests.delete(requestKey)
}
// 处理HTTP错误
handleHttpError(error)
return Promise.reject(error)
}
)// 处理HTTP错误
const handleHttpError = (error) => {
if (axios.isCancel(error)) {
console.log('请求被取消:', error.message)
return
}
const { response, code, message } = error
if (response) {
// 服务器响应错误
const { status, data } = response
switch (status) {
case 400:
ElMessage.error(data?.msg || '请求参数错误')
break
case 401:
handleUnauthorized()
break
case 403:
ElMessage.error('权限不足,拒绝访问')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 408:
ElMessage.error('请求超时')
break
case 500:
ElMessage.error('服务器内部错误')
break
case 502:
ElMessage.error('网关错误')
break
case 503:
ElMessage.error('服务不可用')
break
case 504:
ElMessage.error('网关超时')
break
default:
ElMessage.error(data?.msg || `请求失败 (${status})`)
}
} else if (code === 'ECONNABORTED') {
// 请求超时
ElMessage.error('请求超时,请稍后重试')
} else if (code === 'ERR_NETWORK') {
// 网络错误
ElMessage.error('网络连接失败,请检查网络设置')
} else {
// 其他错误
ElMessage.error(message || '网络请求失败')
}
// 发送错误报告
reportError(error)
}
// 处理业务错误
const handleBusinessError = (data, config) => {
const { code, msg } = data
// 根据业务错误码处理
switch (code) {
case 40001:
ElMessage.error('参数验证失败')
break
case 40003:
ElMessage.error('数据不存在')
break
case 50001:
ElMessage.error('服务器内部错误')
break
default:
if (config.showError !== false) {
ElMessage.error(msg || '操作失败')
}
}
}
// 处理未授权
const handleUnauthorized = () => {
const userStore = useUserStore()
ElMessageBox.confirm(
'登录状态已过期,您可以继续留在该页面,或者重新登录',
'系统提示',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
userStore.logout()
}).catch(() => {
// 用户取消
})
}// 生成请求ID
const generateRequestId = () => {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
// 生成请求键
const generateRequestKey = (config) => {
const { method, url, params, data } = config
return `${method}:${url}:${JSON.stringify(params)}:${JSON.stringify(data)}`
}
// 获取语言设置
const getLanguage = () => {
return localStorage.getItem('language') || 'zh-CN'
}
// 显示加载状态
let loadingInstance = null
let loadingCount = 0
const showLoading = () => {
loadingCount++
if (loadingCount === 1) {
loadingInstance = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
}
// 隐藏加载状态
const hideLoading = () => {
loadingCount--
if (loadingCount <= 0) {
loadingCount = 0
if (loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
}
// 待处理请求映射
const pendingRequests = new Map()
// 取消所有待处理请求
const cancelAllRequests = () => {
pendingRequests.forEach((cancelToken) => {
cancelToken.cancel('页面切换,取消请求')
})
pendingRequests.clear()
}// 请求重试配置
const retryConfig = {
retries: 3, // 重试次数
retryDelay: 1000, // 重试延迟
retryCondition: (error) => {
// 重试条件
return error.code === 'ECONNABORTED' ||
error.code === 'ERR_NETWORK' ||
(error.response && error.response.status >= 500)
}
}
// 添加重试拦截器
request.interceptors.response.use(
(response) => response,
async (error) => {
const config = error.config
// 如果没有配置重试或已达到最大重试次数
if (!config || !retryConfig.retries || config.__retryCount >= retryConfig.retries) {
return Promise.reject(error)
}
// 检查是否满足重试条件
if (!retryConfig.retryCondition(error)) {
return Promise.reject(error)
}
// 增加重试计数
config.__retryCount = config.__retryCount || 0
config.__retryCount++
// 延迟重试
await new Promise(resolve => {
setTimeout(resolve, retryConfig.retryDelay * config.__retryCount)
})
console.log(`请求重试 ${config.__retryCount}/${retryConfig.retries}: ${config.url}`)
// 重新发起请求
return request(config)
}
)// 请求缓存
const requestCache = new Map()
// 缓存配置
const cacheConfig = {
maxAge: 5 * 60 * 1000, // 缓存时间5分钟
maxSize: 100 // 最大缓存数量
}
// 获取缓存键
const getCacheKey = (config) => {
const { method, url, params } = config
return `${method}:${url}:${JSON.stringify(params)}`
}
// 获取缓存数据
const getCachedResponse = (config) => {
if (config.method !== 'get' || config.cache === false) {
return null
}
const cacheKey = getCacheKey(config)
const cached = requestCache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < cacheConfig.maxAge) {
return cached.data
}
return null
}
// 设置缓存数据
const setCachedResponse = (config, data) => {
if (config.method !== 'get' || config.cache === false) {
return
}
const cacheKey = getCacheKey(config)
// 检查缓存大小
if (requestCache.size >= cacheConfig.maxSize) {
// 删除最旧的缓存
const firstKey = requestCache.keys().next().value
requestCache.delete(firstKey)
}
requestCache.set(cacheKey, {
data,
timestamp: Date.now()
})
}
// 在请求拦截器中检查缓存
request.interceptors.request.use((config) => {
const cachedResponse = getCachedResponse(config)
if (cachedResponse) {
// 返回缓存数据
return Promise.resolve({
data: cachedResponse,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {}
})
}
return config
})
// 在响应拦截器中设置缓存
request.interceptors.response.use((response) => {
setCachedResponse(response.config, response.data)
return response
})// 文件上传
const uploadFile = (url, file, options = {}) => {
const formData = new FormData()
formData.append('file', file)
// 添加额外参数
if (options.data) {
Object.keys(options.data).forEach(key => {
formData.append(key, options.data[key])
})
}
return request({
url,
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: options.onProgress,
...options
})
}
// 文件下载
const downloadFile = (url, params = {}, options = {}) => {
return request({
url,
method: 'get',
params,
responseType: 'blob',
...options
}).then(response => {
// 创建下载链接
const blob = new Blob([response.data])
const downloadUrl = window.URL.createObjectURL(blob)
// 获取文件名
const contentDisposition = response.headers['content-disposition']
let filename = options.filename || 'download'
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1].replace(/['"]/g, '')
}
}
// 创建下载元素
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename
document.body.appendChild(link)
link.click()
// 清理
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
return response
})
}// Mock数据配置
const mockConfig = {
enabled: import.meta.env.VITE_USE_MOCK === 'true',
delay: 500 // 模拟延迟
}
// Mock拦截器
if (mockConfig.enabled) {
request.interceptors.request.use(async (config) => {
// 检查是否有对应的Mock数据
const mockData = await getMockData(config)
if (mockData) {
// 模拟延迟
await new Promise(resolve => setTimeout(resolve, mockConfig.delay))
// 返回Mock响应
return Promise.resolve({
data: mockData,
status: 200,
statusText: 'OK',
headers: {},
config,
request: {}
})
}
return config
})
}
// 获取Mock数据
const getMockData = async (config) => {
try {
const mockModule = await import('@/mock/api.js')
const mockApis = mockModule.default || mockModule
const mockApi = mockApis.find(api => {
return api.url === config.url && api.method === config.method
})
return mockApi ? mockApi.response : null
} catch (error) {
console.warn('获取Mock数据失败:', error)
return null
}
}// 性能监控
const performanceMonitor = {
// 请求统计
stats: {
totalRequests: 0,
successRequests: 0,
failedRequests: 0,
totalTime: 0,
slowRequests: []
},
// 记录请求
recordRequest: (config, response, error) => {
const duration = Date.now() - config.startTime
performanceMonitor.stats.totalRequests++
performanceMonitor.stats.totalTime += duration
if (error) {
performanceMonitor.stats.failedRequests++
} else {
performanceMonitor.stats.successRequests++
}
// 记录慢请求
if (duration > 3000) {
performanceMonitor.stats.slowRequests.push({
url: config.url,
method: config.method,
duration,
timestamp: Date.now()
})
}
},
// 获取统计信息
getStats: () => {
const stats = performanceMonitor.stats
return {
...stats,
averageTime: stats.totalRequests > 0 ? stats.totalTime / stats.totalRequests : 0,
successRate: stats.totalRequests > 0 ? (stats.successRequests / stats.totalRequests) * 100 : 0
}
},
// 重置统计
resetStats: () => {
performanceMonitor.stats = {
totalRequests: 0,
successRequests: 0,
failedRequests: 0,
totalTime: 0,
slowRequests: []
}
}
}
// 在响应拦截器中记录性能数据
request.interceptors.response.use(
(response) => {
performanceMonitor.recordRequest(response.config, response)
return response
},
(error) => {
if (error.config) {
performanceMonitor.recordRequest(error.config, null, error)
}
return Promise.reject(error)
}
)import request from '@/utils/request'
// GET请求
const getUsers = (params) => {
return request({
url: '/users',
method: 'get',
params
})
}
// POST请求
const createUser = (data) => {
return request({
url: '/users',
method: 'post',
data
})
}
// PUT请求
const updateUser = (id, data) => {
return request({
url: `/users/${id}`,
method: 'put',
data
})
}
// DELETE请求
const deleteUser = (id) => {
return request({
url: `/users/${id}`,
method: 'delete'
})
}// 带配置的请求
const getUsersWithConfig = (params) => {
return request({
url: '/users',
method: 'get',
params,
timeout: 5000, // 自定义超时时间
showLoading: false, // 不显示加载状态
showError: false, // 不显示错误提示
cache: true, // 启用缓存
retry: 3 // 重试次数
})
}
// 文件上传
const uploadAvatar = (file, onProgress) => {
return uploadFile('/upload/avatar', file, {
onProgress,
data: {
type: 'avatar'
}
})
}
// 文件下载
const downloadReport = (params) => {
return downloadFile('/reports/export', params, {
filename: 'report.xlsx'
})
}<script setup>
import { ref } from 'vue'
import { getUsers, createUser } from '@/api/user'
const users = ref([])
const loading = ref(false)
const loadUsers = async () => {
try {
loading.value = true
const result = await getUsers({ page: 1, size: 20 })
users.value = result.data.list
} catch (error) {
console.error('加载用户失败:', error)
} finally {
loading.value = false
}
}
const addUser = async (userData) => {
try {
await createUser(userData)
ElMessage.success('用户创建成功')
loadUsers() // 重新加载列表
} catch (error) {
// 错误已在拦截器中处理
}
}
</script>- 错误处理: 统一的错误处理机制,避免重复处理
- 请求去重: 防止重复请求造成的问题
- 性能优化: 合理使用缓存和重试机制
- 安全性: 敏感信息不要在请求中暴露
- 监控: 监控请求性能和错误率
最后更新时间:2025-09-19