一、为什么选择 Axios?
在项目开发中,网络请求是必不可少的一环。虽然浏览器提供了 Fetch API 和 XMLHttpRequest,但 Axios 凭借其强大的功能和友好的 API,成为最受欢迎的请求库。
1.1 Axios 核心优势
// 1. 支持浏览器和 Node.js 环境
// 浏览器:XMLHttpRequest
// Node.js:http 模块
// 2. 自动转换 JSON 数据
axios.get('/api/user').then(res => {
console.log(res.data) // 自动解析为 JavaScript 对象
})
// 3. 请求拦截和响应拦截
// 4. 取消请求
// 5. 超时处理
// 6. 并发请求
// 7. CSRF 防护
// 8. 上传/下载进度监控
二、Axios 基础入门
2.1 安装与引入
# 使用 npm
npm install axios
# 使用 yarn
yarn add axios
# 使用 CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
2.2 基本请求方法
import axios from 'axios'
// GET 请求
axios.get('/api/users', {
params: {
page: 1,
limit: 10
}
})
.then(response => {
console.log('用户列表:', response.data)
})
.catch(error => {
console.error('请求失败:', error)
})
// POST 请求
axios.post('/api/users', {
name: '张三',
email: 'zhangsan@example.com',
age: 25
})
.then(response => {
console.log('创建成功:', response.data)
})
// PUT 请求(更新)
axios.put('/api/users/1', {
name: '张三丰',
age: 26
})
// DELETE 请求
axios.delete('/api/users/1')
// PATCH 请求(部分更新)
axios.patch('/api/users/1', {
age: 27
})
2.3 请求配置详解
axios({
method: 'post', // 请求方法
url: '/api/users', // 请求地址
baseURL: 'https://api.example.com', // 基础URL
headers: { // 请求头
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
params: { // URL参数
page: 1,
limit: 10
},
data: { // 请求体
name: '张三'
},
timeout: 5000, // 超时时间(ms)
withCredentials: true, // 跨域请求时携带cookie
responseType: 'json', // 响应数据类型
maxContentLength: 2000, // 最大响应长度
validateStatus: function (status) { // 定义哪些状态码是成功的
return status >= 200 && status < 300
},
proxy: { // 代理配置
host: '127.0.0.1',
port: 9000
}
})
2.4 响应数据结构
axios.get('/api/user').then(response => {
// response 对象包含:
console.log(response.data) // 服务器返回的数据
console.log(response.status) // HTTP 状态码
console.log(response.statusText) // 状态消息
console.log(response.headers) // 响应头
console.log(response.config) // 请求配置
console.log(response.request) // 原生XMLHttpRequest对象
})
三、项目中的 Axios 封装
在实际项目中,我们通常会对 Axios 进行二次封装,统一处理请求配置、拦截器、错误处理等。
3.1 基础封装结构
// service/index.js
import axios from 'axios'
class RequestService {
constructor() {
// 创建 axios 实例
this.service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 初始化拦截器
this.setupInterceptors()
}
setupInterceptors() {
// 请求拦截器
this.service.interceptors.request.use(
this.handleRequestSuccess,
this.handleRequestError
)
// 响应拦截器
this.service.interceptors.response.use(
this.handleResponseSuccess,
this.handleResponseError
)
}
handleRequestSuccess(config) {
console.log('请求配置:', config)
return config
}
handleRequestError(error) {
console.error('请求错误:', error)
return Promise.reject(error)
}
handleResponseSuccess(response) {
console.log('响应数据:', response)
return response
}
handleResponseError(error) {
console.error('响应错误:', error)
return Promise.reject(error)
}
// 请求方法封装
get(url, params = {}, config = {}) {
return this.service.get(url, { params, ...config })
}
post(url, data = {}, config = {}) {
return this.service.post(url, data, config)
}
put(url, data = {}, config = {}) {
return this.service.put(url, data, config)
}
delete(url, params = {}, config = {}) {
return this.service.delete(url, { params, ...config })
}
patch(url, data = {}, config = {}) {
return this.service.patch(url, data, config)
}
}
export default new RequestService()
3.2 完整的拦截器实现
// service/interceptors.js
import { message, Modal } from 'ant-design-vue'
import router from '@/router'
import store from '@/store'
// 请求拦截器
export function requestSuccess(config) {
// 1. 添加 token
const token = store.state.user.token
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
// 2. 添加时间戳防止缓存(GET请求)
if (config.method === 'get') {
config.params = {
...config.params,
_t: Date.now()
}
}
// 3. 请求日志(开发环境)
if (process.env.NODE_ENV === 'development') {
console.log('🚀 请求信息:', {
url: config.url,
method: config.method,
params: config.params,
data: config.data,
headers: config.headers
})
}
return config
}
export function requestError(error) {
console.error('❌ 请求发送失败:', error)
message.error('网络请求失败,请检查网络连接')
return Promise.reject(error)
}
// 响应拦截器
export function responseSuccess(response) {
// 可以统一处理业务状态码
const { code, data, message: msg } = response.data
// 根据后端约定的状态码处理
switch (code) {
case 200: // 成功
return data
case 401: // 未授权
handleUnauthorized()
return Promise.reject(new Error('未授权,请重新登录'))
case 403: // 禁止访问
message.error('没有权限访问')
return Promise.reject(new Error('禁止访问'))
case 500: // 服务器错误
message.error('服务器错误,请稍后重试')
return Promise.reject(new Error('服务器错误'))
default:
// 其他错误
message.error(msg || '请求失败')
return Promise.reject(new Error(msg || '请求失败'))
}
}
export function responseError(error) {
// 处理 HTTP 状态码错误
if (error.response) {
// 服务器返回了错误状态码
const { status, data } = error.response
switch (status) {
case 400:
message.error(data?.message || '请求参数错误')
break
case 401:
handleUnauthorized()
break
case 403:
message.error('没有权限访问')
break
case 404:
message.error('请求的资源不存在')
break
case 500:
message.error('服务器内部错误')
break
case 502:
message.error('网关错误')
break
case 503:
message.error('服务不可用')
break
case 504:
message.error('网关超时')
break
default:
message.error(`网络错误: ${status}`)
}
} else if (error.request) {
// 请求已发送但没有收到响应
message.error('服务器无响应,请检查网络')
} else {
// 请求配置出错
message.error('请求配置错误')
}
return Promise.reject(error)
}
// 处理未授权
function handleUnauthorized() {
Modal.confirm({
title: '登录已过期',
content: '您的登录信息已过期,请重新登录',
okText: '去登录',
cancelText: '取消',
onOk: () => {
store.dispatch('user/logout')
router.push('/login')
}
})
}
3.3 增强版封装(支持取消请求、重试)
// service/advanced.js
import axios from 'axios'
import qs from 'qs'
class AdvancedRequest {
constructor() {
this.service = axios.create({
baseURL: process.env.VUE_APP_API_URL,
timeout: 30000,
paramsSerializer: params => {
// 处理复杂参数序列化
return qs.stringify(params, { indices: false })
}
})
// 存储取消请求的控制器
this.pendingRequests = new Map()
this.setupInterceptors()
}
setupInterceptors() {
// 请求拦截器
this.service.interceptors.request.use(
config => {
// 添加取消请求功能
this.addCancelToken(config)
// 请求签名
if (config.needSign) {
config.data = this.signRequest(config)
}
// 加密敏感数据
if (config.encrypt) {
config.data = this.encryptData(config.data)
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
this.service.interceptors.response.use(
response => {
// 请求完成后移除 pending 记录
this.removePendingRequest(response.config)
// 处理文件下载
if (response.config.responseType === 'blob') {
return this.handleFileResponse(response)
}
return response.data
},
error => {
// 如果是取消请求,不处理错误
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message)
return Promise.reject(error)
}
// 移除 pending 记录
if (error.config) {
this.removePendingRequest(error.config)
}
return Promise.reject(error)
}
)
}
// 取消请求管理
addCancelToken(config) {
// 避免重复请求
const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
// 如果已有相同请求,取消之前的请求
if (this.pendingRequests.has(requestKey)) {
const cancel = this.pendingRequests.get(requestKey)
cancel('重复请求已取消')
this.pendingRequests.delete(requestKey)
}
// 创建新的取消令牌
config.cancelToken = new axios.CancelToken(cancel => {
this.pendingRequests.set(requestKey, cancel)
})
}
removePendingRequest(config) {
const requestKey = `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
if (this.pendingRequests.has(requestKey)) {
this.pendingRequests.delete(requestKey)
}
}
// 取消所有请求
cancelAllRequests() {
this.pendingRequests.forEach(cancel => cancel('主动取消所有请求'))
this.pendingRequests.clear()
}
// 带重试机制的请求
async requestWithRetry(config, retries = 3) {
let lastError
for (let i = 0; i < retries; i++) {
try {
const response = await this.service(config)
return response
} catch (error) {
lastError = error
// 是否应该重试
if (this.shouldRetry(error, i, retries)) {
// 指数退避延迟
const delay = Math.pow(2, i) * 1000
console.log(`第${i + 1}次请求失败,${delay}ms后重试...`)
await this.sleep(delay)
continue
}
break
}
}
throw lastError
}
shouldRetry(error, currentRetry, maxRetries) {
// 只有特定错误才重试
if (axios.isCancel(error)) return false
// 网络错误或超时重试
const retryableErrors = [
'ECONNABORTED', // 超时
'ETIMEDOUT', // 连接超时
'ECONNREFUSED', // 连接被拒绝
'ECONNRESET', // 连接重置
'ENOTFOUND' // DNS解析失败
]
const shouldRetry = (
currentRetry < maxRetries - 1 &&
(error.code && retryableErrors.includes(error.code)) ||
(error.response && error.response.status >= 500)
)
return shouldRetry
}
// 文件上传(支持进度)
uploadFile(url, file, onProgress) {
const formData = new FormData()
formData.append('file', file)
return this.service.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: progressEvent => {
if (onProgress) {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress(percentCompleted)
}
}
})
}
// 文件下载
async downloadFile(url, filename) {
const response = await this.service.get(url, {
responseType: 'blob'
})
// 创建下载链接
const blob = new Blob([response.data])
const downloadUrl = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = filename || 'download'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(downloadUrl)
}
handleFileResponse(response) {
const contentDisposition = response.headers['content-disposition']
let filename = 'download'
if (contentDisposition) {
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (match && match[1]) {
filename = match[1].replace(/['"]/g, '')
// 处理中文文件名
try {
filename = decodeURIComponent(escape(filename))
} catch (e) {
console.error('文件名解码失败', e)
}
}
}
return {
data: response.data,
filename,
type: response.headers['content-type']
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
signRequest(config) {
// 实现请求签名逻辑
const timestamp = Date.now()
const nonce = Math.random().toString(36).substring(7)
const data = { ...config.data, timestamp, nonce }
// 签名计算...
// data.sign = generateSignature(data)
return data
}
encryptData(data) {
// 实现数据加密逻辑
return data
}
}
export default new AdvancedRequest()
四、业务层封装
4.1 API 模块化管理
// api/modules/user.js
import request from '@/service'
// 用户相关接口
export const userApi = {
// 登录
login(data) {
return request.post('/auth/login', data, {
needSign: true // 需要签名
})
},
// 登出
logout() {
return request.post('/auth/logout')
},
// 获取用户信息
getUserInfo() {
return request.get('/user/info', {}, {
retry: 3 // 失败重试3次
})
},
// 更新用户信息
updateUserInfo(data) {
return request.put('/user/info', data)
},
// 上传头像
uploadAvatar(file, onProgress) {
return request.uploadFile('/user/avatar', file, onProgress)
},
// 获取用户列表
getUserList(params) {
return request.get('/user/list', params, {
cache: true // 启用缓存
})
},
// 导出用户数据
exportUsers(params) {
return request.get('/user/export', params, {
responseType: 'blob'
})
}
}
// api/modules/product.js
export const productApi = {
getProductList(params) {
return request.get('/product/list', params)
},
getProductDetail(id) {
return request.get(`/product/detail/${id}`)
},
createProduct(data) {
return request.post('/product', data)
},
updateProduct(id, data) {
return request.put(`/product/${id}`, data)
},
deleteProduct(id) {
return request.delete(`/product/${id}`)
}
}
// api/index.js
export { userApi } from './modules/user'
export { productApi } from './modules/product'
4.2 请求缓存管理
// service/cache.js
class RequestCache {
constructor() {
this.cache = new Map()
this.maxAge = 5 * 60 * 1000 // 默认5分钟
}
// 生成缓存key
generateKey(config) {
return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`
}
// 设置缓存
set(key, data, maxAge = this.maxAge) {
this.cache.set(key, {
data,
timestamp: Date.now(),
maxAge
})
}
// 获取缓存
get(key) {
const cached = this.cache.get(key)
if (!cached) return null
// 检查是否过期
if (Date.now() - cached.timestamp > cached.maxAge) {
this.cache.delete(key)
return null
}
return cached.data
}
// 清除缓存
clear() {
this.cache.clear()
}
// 清除指定缓存
delete(key) {
this.cache.delete(key)
}
// 清除匹配模式的缓存
clearPattern(pattern) {
const regex = new RegExp(pattern)
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.cache.delete(key)
}
}
}
}
export default new RequestCache()
4.3 在 Vue/React 中使用
// Vue 3 中使用
import { userApi } from '@/api'
import { ref, onMounted } from 'vue'
export default {
setup() {
const userList = ref([])
const loading = ref(false)
const fetchUserList = async () => {
loading.value = true
try {
const res = await userApi.getUserList({
page: 1,
limit: 10
})
userList.value = res
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUserList()
})
return {
userList,
loading,
fetchUserList
}
}
}
// React 中使用
import { useState, useEffect } from 'react'
import { userApi } from '@/api'
function UserList() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
const fetchUsers = async () => {
setLoading(true)
try {
const data = await userApi.getUserList()
setUsers(data)
} catch (error) {
console.error('Failed to fetch users:', error)
} finally {
setLoading(false)
}
}
fetchUsers()
}, [])
return (
<div>
{loading ? <Spin /> : (
<Table dataSource={users} />
)}
</div>
)
}
五、高级功能实现
5.1 请求队列管理
// service/queue.js
class RequestQueue {
constructor(concurrency = 5) {
this.concurrency = concurrency
this.queue = []
this.running = 0
}
// 添加请求到队列
add(request) {
return new Promise((resolve, reject) => {
this.queue.push({
request,
resolve,
reject
})
this.next()
})
}
// 执行下一个请求
next() {
while (this.running < this.concurrency && this.queue.length) {
const { request, resolve, reject } = this.queue.shift()
this.running++
request()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--
this.next()
})
}
}
// 清空队列
clear() {
this.queue = []
this.running = 0
}
// 获取队列状态
getStatus() {
return {
queueLength: this.queue.length,
running: this.running,
concurrency: this.concurrency
}
}
}
export default RequestQueue
5.2 请求节流防抖
// service/throttle.js
class RequestThrottle {
constructor() {
this.pendingRequests = new Map()
}
// 防抖:最后一次请求有效
debounce(key, fn, delay = 300) {
if (this.pendingRequests.has(key)) {
clearTimeout(this.pendingRequests.get(key))
}
const timeout = setTimeout(() => {
fn()
this.pendingRequests.delete(key)
}, delay)
this.pendingRequests.set(key, timeout)
}
// 节流:限制请求频率
throttle(key, fn, limit = 1000) {
const now = Date.now()
const lastCall = this.pendingRequests.get(key)
if (!lastCall || now - lastCall > limit) {
fn()
this.pendingRequests.set(key, now)
}
}
// 取消所有待执行的请求
cancelAll() {
this.pendingRequests.forEach(timeout => {
clearTimeout(timeout)
})
this.pendingRequests.clear()
}
}
export default new RequestThrottle()
5.3 断网重连机制
// service/reconnect.js
class ReconnectManager {
constructor(requestService) {
this.requestService = requestService
this.isOnline = navigator.onLine
this.reconnectAttempts = 0
this.maxReconnectAttempts = 5
this.pendingRequests = []
this.initEventListeners()
}
initEventListeners() {
window.addEventListener('online', () => {
this.handleOnline()
})
window.addEventListener('offline', () => {
this.handleOffline()
})
}
handleOnline() {
console.log('网络已恢复,开始重连...')
this.isOnline = true
this.reconnectAttempts = 0
// 重试所有待处理的请求
this.processPendingRequests()
}
handleOffline() {
console.log('网络已断开')
this.isOnline = false
}
async processPendingRequests() {
while (this.pendingRequests.length > 0) {
const request = this.pendingRequests.shift()
try {
const result = await this.requestService(request.config)
request.resolve(result)
} catch (error) {
request.reject(error)
}
}
}
// 添加请求到待处理队列
addPendingRequest(config) {
return new Promise((resolve, reject) => {
this.pendingRequests.push({
config,
resolve,
reject
})
// 尝试重新连接
this.attemptReconnect()
})
}
attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('重连失败次数过多,请手动刷新')
return
}
this.reconnectAttempts++
setTimeout(() => {
if (navigator.onLine) {
this.handleOnline()
}
}, Math.pow(2, this.reconnectAttempts) * 1000)
}
}
六、错误处理与日志
6.1 统一错误处理
// service/errorHandler.js
class ErrorHandler {
constructor() {
this.errorListeners = []
}
// 处理错误
handle(error, context = {}) {
// 格式化错误信息
const errorInfo = this.formatError(error, context)
// 记录错误日志
this.logError(errorInfo)
// 触发错误监听器
this.notifyListeners(errorInfo)
// 根据错误类型进行处理
this.processByType(errorInfo)
return errorInfo
}
formatError(error, context) {
return {
timestamp: new Date().toISOString(),
type: this.getErrorType(error),
message: error.message,
code: error.code,
status: error.response?.status,
url: error.config?.url,
method: error.config?.method,
params: error.config?.params,
data: error.config?.data,
stack: error.stack,
context
}
}
getErrorType(error) {
if (error.response) {
// 服务器返回错误状态码
const status = error.response.status
if (status >= 500) return 'SERVER_ERROR'
if (status === 401) return 'UNAUTHORIZED'
if (status === 403) return 'FORBIDDEN'
if (status === 404) return 'NOT_FOUND'
if (status >= 400) return 'CLIENT_ERROR'
} else if (error.request) {
// 请求已发送但没有响应
return 'NETWORK_ERROR'
} else {
// 请求配置错误
return 'CONFIG_ERROR'
}
return 'UNKNOWN_ERROR'
}
logError(errorInfo) {
// 开发环境打印到控制台
if (process.env.NODE_ENV === 'development') {
console.group('❌ 请求错误')
console.log('时间:', errorInfo.timestamp)
console.log('类型:', errorInfo.type)
console.log('信息:', errorInfo.message)
console.log('状态码:', errorInfo.status)
console.log('URL:', errorInfo.url)
console.log('方法:', errorInfo.method)
console.log('参数:', errorInfo.params)
console.log('数据:', errorInfo.data)
console.trace('堆栈:', errorInfo.stack)
console.groupEnd()
}
// 生产环境发送到日志服务
if (process.env.NODE_ENV === 'production') {
this.sendToLogService(errorInfo)
}
}
sendToLogService(errorInfo) {
// 发送错误日志到服务器
fetch('/api/log/error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorInfo),
keepalive: true // 即使页面卸载也发送
}).catch(() => {
// 静默失败
})
}
processByType(errorInfo) {
switch (errorInfo.type) {
case 'UNAUTHORIZED':
// 跳转到登录页
this.redirectToLogin()
break
case 'SERVER_ERROR':
// 显示服务器错误提示
this.showErrorMessage('服务器开小差了,请稍后重试')
break
case 'NETWORK_ERROR':
// 显示网络错误提示
this.showErrorMessage('网络连接失败,请检查网络设置')
break
default:
// 显示通用错误提示
this.showErrorMessage('操作失败,请重试')
}
}
addListener(listener) {
this.errorListeners.push(listener)
}
notifyListeners(errorInfo) {
this.errorListeners.forEach(listener => {
try {
listener(errorInfo)
} catch (e) {
console.error('Error listener failed:', e)
}
})
}
redirectToLogin() {
// 跳转到登录页
if (window.location.pathname !== '/login') {
window.location.href = '/login'
}
}
showErrorMessage(message) {
// 使用UI库的提示组件
if (window.$message) {
window.$message.error(message)
} else {
alert(message)
}
}
}
export default new ErrorHandler()
6.2 请求监控
// service/monitor.js
class RequestMonitor {
constructor() {
this.metrics = {
totalRequests: 0,
successRequests: 0,
failedRequests: 0,
totalTime: 0,
slowRequests: [],
errorStats: {}
}
this.slowThreshold = 3000 // 慢请求阈值(ms)
}
// 记录请求开始
startRequest(config) {
const requestId = this.generateRequestId()
const startTime = Date.now()
config.metadata = {
requestId,
startTime
}
return config
}
// 记录请求结束
endRequest(config, response, error) {
const endTime = Date.now()
const startTime = config.metadata?.startTime || endTime
const duration = endTime - startTime
// 更新总请求数
this.metrics.totalRequests++
if (error) {
// 记录失败请求
this.metrics.failedRequests++
this.recordError(config, error)
} else {
// 记录成功请求
this.metrics.successRequests++
this.metrics.totalTime += duration
}
// 检查慢请求
if (duration > this.slowThreshold) {
this.recordSlowRequest(config, duration, error)
}
// 打印性能日志
this.logPerformance(config, duration, error)
// 清理metadata
delete config.metadata
}
recordError(config, error) {
const errorType = error.response?.status || 'NETWORK_ERROR'
this.metrics.errorStats[errorType] = (this.metrics.errorStats[errorType] || 0) + 1
}
recordSlowRequest(config, duration, error) {
this.metrics.slowRequests.push({
url: config.url,
method: config.method,
duration,
timestamp: new Date().toISOString(),
success: !error,
error: error?.message
})
// 保留最近100条慢请求记录
if (this.metrics.slowRequests.length > 100) {
this.metrics.slowRequests.shift()
}
}
logPerformance(config, duration, error) {
const status = error ? '❌' : '✅'
const slow = duration > this.slowThreshold ? '🐢' : ''
console.log(
`${status} ${slow} [${config.method.toUpperCase()}] ${config.url} - ${duration}ms`
)
}
generateRequestId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
// 获取监控报告
getReport() {
const avgTime = this.metrics.successRequests > 0
? Math.round(this.metrics.totalTime / this.metrics.successRequests)
: 0
return {
...this.metrics,
avgTime,
successRate: this.metrics.totalRequests > 0
? `${Math.round((this.metrics.successRequests / this.metrics.totalRequests) * 100)}%`
: '0%'
}
}
// 重置监控数据
reset() {
this.metrics = {
totalRequests: 0,
successRequests: 0,
failedRequests: 0,
totalTime: 0,
slowRequests: [],
errorStats: {}
}
}
}
export default new RequestMonitor()
七、测试与调试
7.1 单元测试
// service/__tests__/request.test.js
import MockAdapter from 'axios-mock-adapter'
import request from '../index'
import axios from 'axios'
describe('Request Service', () => {
let mock
beforeEach(() => {
mock = new MockAdapter(axios)
})
afterEach(() => {
mock.reset()
})
test('should handle GET request successfully', async () => {
const mockData = { id: 1, name: '张三' }
mock.onGet('/api/user/1').reply(200, mockData)
const result = await request.get('/api/user/1')
expect(result).toEqual(mockData)
})
test('should handle request error', async () => {
mock.onGet('/api/user/1').reply(500)
await expect(request.get('/api/user/1')).rejects.toThrow()
})
test('should add token to headers', async () => {
const token = 'test-token'
localStorage.setItem('token', token)
mock.onGet('/api/user').reply(config => {
expect(config.headers.Authorization).toBe(`Bearer ${token}`)
return [200, {}]
})
await request.get('/api/user')
})
test('should handle timeout', async () => {
mock.onGet('/api/user').timeout()
await expect(request.get('/api/user')).rejects.toThrow('timeout')
}, 10000)
})
7.2 调试技巧
// 调试配置
if (process.env.NODE_ENV === 'development') {
// 开启调试模式
axios.defaults.debug = true
// 拦截所有请求并打印详细信息
axios.interceptors.request.use(config => {
console.group(`🌐 请求 [${config.method}] ${config.url}`)
console.log('参数:', config.params)
console.log('数据:', config.data)
console.log('头信息:', config.headers)
console.groupEnd()
return config
})
// 模拟慢网络
if (process.env.VUE_APP_SLOW_NETWORK) {
axios.interceptors.request.use(async config => {
await new Promise(resolve => setTimeout(resolve, 2000))
return config
})
}
// 模拟随机失败
if (process.env.VUE_APP_RANDOM_FAIL) {
axios.interceptors.response.use(
response => response,
error => {
if (Math.random() < 0.1) { // 10% 概率失败
return Promise.reject(new Error('模拟网络错误'))
}
return Promise.reject(error)
}
)
}
}
八、最佳实践总结
8.1 项目结构推荐
src/
├── api/
│ ├── modules/
│ │ ├── user.js
│ │ ├── product.js
│ │ └── order.js
│ ├── index.js
│ └── config.js
├── service/
│ ├── index.js # 请求服务主入口
│ ├── interceptors.js # 拦截器
│ ├── errorHandler.js # 错误处理
│ ├── cache.js # 缓存管理
│ ├── monitor.js # 监控
│ └── utils.js # 工具函数
└── utils/
└── request.js # 导出封装的请求方法
8.2 配置管理
// service/config.js
const config = {
development: {
baseURL: 'http://localhost:3000/api',
timeout: 10000,
withCredentials: true
},
test: {
baseURL: 'https://test-api.example.com/api',
timeout: 15000
},
production: {
baseURL: 'https://api.example.com/api',
timeout: 30000,
withCredentials: true
}
}
export default config[process.env.NODE_ENV || 'development']
8.3 安全建议
// 1. 防止 CSRF 攻击
axios.defaults.xsrfCookieName = 'csrf-token'
axios.defaults.xsrfHeaderName = 'X-CSRF-Token'
// 2. 敏感信息加密
import CryptoJS from 'crypto-js'
function encryptRequest(data) {
const key = CryptoJS.enc.Utf8.parse(process.env.VUE_APP_SECRET_KEY)
const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}
// 3. HTTPS 强制
if (window.location.protocol !== 'https:' && process.env.NODE_ENV === 'production') {
window.location.href = 'https://' + window.location.host + window.location.pathname
}
九、常见面试题
Q1: 如何取消重复请求?
// 使用 CancelToken
const cancelTokenSource = axios.CancelToken.source()
axios.get('/api/user', {
cancelToken: cancelTokenSource.token
})
// 取消请求
cancelTokenSource.cancel('操作取消')
Q2: 如何实现请求缓存?
// 使用 Map 存储响应结果
const cache = new Map()
async function requestWithCache(url) {
if (cache.has(url)) {
return cache.get(url)
}
const response = await axios.get(url)
cache.set(url, response.data)
return response.data
}
Q3: 如何统一处理错误?
// 响应拦截器中统一处理
axios.interceptors.response.use(
response => response,
error => {
if (error.response) {
switch (error.response.status) {
case 401:
// 处理未授权
break
case 404:
// 处理未找到
break
case 500:
// 处理服务器错误
break
}
}
return Promise.reject(error)
}
)
Q4: 如何监控请求性能?
// 使用 Performance API
const start = performance.now()
axios.get('/api/data').then(() => {
const end = performance.now()
console.log(`请求耗时: ${end - start}ms`)
})
十、总结
Axios 封装的核心要点:
-
统一配置:baseURL、超时、请求头等
-
拦截器:请求/响应拦截,统一处理 token、日志、错误
-
错误处理:分类处理、友好提示、日志记录
-
取消请求:防止重复提交、组件卸载时取消
-
重试机制:网络错误时自动重试
-
缓存管理:减少重复请求
-
监控告警:性能监控、错误上报
-
测试覆盖:单元测试保证稳定性
通过合理的封装,可以让项目中的网络请求更加健壮、可维护,提升开发效率和用户体验。