1. 项目概述
在现代前端开发中,网络请求是连接用户界面与后端服务的桥梁。Vue作为主流的前端框架,与Axios的结合使用已经成为开发者处理HTTP请求的黄金标准。但直接使用原生Axios会导致代码重复、难以维护,特别是在大型项目中,统一的请求管理和错误处理机制显得尤为重要。
我在多个Vue企业级项目中积累了一套成熟的Axios封装方案,通过请求拦截、响应拦截和统一错误处理,可以显著提升开发效率和代码质量。这种封装不仅减少了重复代码,还实现了以下核心价值:
- 统一处理HTTP状态码和业务错误码
- 自动携带身份验证令牌
- 全局loading状态管理
- 请求参数和响应数据的标准化处理
2. Axios基础封装
2.1 创建Axios实例
首先我们需要创建一个独立的Axios实例,而不是直接使用全局的Axios对象。这样做的好处是可以针对不同后端服务配置不同的实例,同时避免污染全局配置。
// src/utils/request.js import axios from 'axios' // 创建axios实例 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 从环境变量读取API基础路径 timeout: 10000, // 请求超时时间 headers: { 'Content-Type': 'application/json;charset=utf-8' } })注意:baseURL应该通过环境变量配置,这样在不同环境(开发、测试、生产)可以自动切换API地址,避免硬编码。
2.2 请求拦截器设计
请求拦截器的主要作用是在请求发送前对配置进行处理。最常见的场景是自动添加认证token:
// 请求拦截器 service.interceptors.request.use( config => { // 在发送请求之前做些什么 if (store.getters.token) { config.headers['Authorization'] = `Bearer ${store.getters.token}` } return config }, error => { // 对请求错误做些什么 return Promise.reject(error) } )实际项目中,请求拦截器还可以实现:
- 自动添加时间戳防止缓存
- 对特定接口添加自定义header
- 对POST请求数据进行序列化处理
- 文件上传时的特殊content-type设置
3. 响应拦截与错误处理
3.1 响应拦截器实现
响应拦截器是处理服务端返回数据的核心环节。一个完善的响应拦截器应该处理以下几种情况:
service.interceptors.response.use( response => { const res = response.data // 假设业务状态码为200表示成功 if (res.code === 200) { return res.data } else { // 处理业务错误 return handleBusinessError(res) } }, error => { // 处理HTTP错误 return handleHttpError(error) } )3.2 错误处理策略
错误处理应该区分HTTP错误和业务错误:
function handleHttpError(error) { let message = '' if (error.response) { switch (error.response.status) { case 400: message = '请求参数错误' break case 401: message = '未授权,请重新登录' // 触发登出逻辑 break case 403: message = '拒绝访问' break case 404: message = '请求资源不存在' break case 500: message = '服务器内部错误' break default: message = `连接错误 ${error.response.status}` } } else { message = '网络连接异常' } // 统一提示错误 Message.error(message) return Promise.reject(error) } function handleBusinessError(res) { const { code, message } = res // 根据业务错误码进行特定处理 if (code === 50008 || code === 50012 || code === 50014) { // 处理token过期等特定错误 MessageBox.confirm('登录状态已过期,请重新登录', '确认登出', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { store.dispatch('user/resetToken').then(() => { location.reload() }) }) } else { Message.error(message || '系统错误') } return Promise.reject(new Error(message || 'Error')) }4. 高级封装技巧
4.1 请求取消与重复请求拦截
在某些场景下,我们需要防止用户频繁点击导致的重复请求:
// 存储每个请求的取消函数 const pendingMap = new Map() // 生成请求唯一标识 function getPendingKey(config) { return [ config.method, config.url, JSON.stringify(config.params), JSON.stringify(config.data) ].join('&') } // 请求拦截器中添加取消逻辑 config.cancelToken = new axios.CancelToken(cancel => { const key = getPendingKey(config) if (pendingMap.has(key)) { pendingMap.get(key)('取消重复请求') } pendingMap.set(key, cancel) }) // 响应拦截器中移除记录 const key = getPendingKey(response.config) pendingMap.delete(key)4.2 文件上传下载处理
对于文件上传下载需要特殊处理:
// 文件上传封装 export function uploadFile(url, file, onUploadProgress) { const formData = new FormData() formData.append('file', file) return service({ url, method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress }) } // 文件下载封装 export function downloadFile(url, params, filename) { return service({ url, params, method: 'get', responseType: 'blob' }).then(data => { const blob = new Blob([data]) const downloadUrl = window.URL.createObjectURL(blob) const link = document.createElement('a') link.href = downloadUrl link.download = filename document.body.appendChild(link) link.click() window.URL.revokeObjectURL(downloadUrl) document.body.removeChild(link) }) }5. 实际应用与优化
5.1 API模块化组织
随着项目规模扩大,建议按业务模块组织API:
// src/api/user.js import request from '@/utils/request' export function login(data) { return request({ url: '/user/login', method: 'post', data }) } export function getInfo() { return request({ url: '/user/info', method: 'get' }) } // 在组件中使用 import { login } from '@/api/user' login({ username: 'admin', password: '123456' }).then(data => { // 处理登录结果 })5.2 性能优化建议
- 请求缓存:对于不常变化的数据可以添加缓存机制
const cacheMap = new Map() export function getDataWithCache(url, params) { const key = `${url}-${JSON.stringify(params)}` if (cacheMap.has(key)) { return Promise.resolve(cacheMap.get(key)) } return request({ url, params }).then(data => { cacheMap.set(key, data) return data }) }请求节流:对于频繁触发的请求(如搜索框输入)可以添加防抖
批量请求:对于多个关联请求可以使用
axios.all取消无用请求:在组件卸载时取消未完成的请求
6. 常见问题与解决方案
6.1 跨域问题处理
开发环境下常见的跨域问题可以通过以下方式解决:
- 配置代理(vue.config.js):
devServer: { proxy: { '/api': { target: 'http://your-api-server.com', changeOrigin: true, pathRewrite: { '^/api': '' } } } }- 后端配置CORS:
- Access-Control-Allow-Origin
- Access-Control-Allow-Headers
- Access-Control-Allow-Methods
6.2 请求超时问题
处理请求超时的几种策略:
- 根据接口重要性设置不同超时时间
// 重要接口设置较长超时 export function getImportantData() { return request({ url: '/important-data', timeout: 30000 // 30秒 }) }- 实现自动重试机制
function requestWithRetry(config, retry = 3) { return request(config).catch(err => { if (retry <= 0) return Promise.reject(err) if (err.code === 'ECONNABORTED') { return requestWithRetry(config, retry - 1) } return Promise.reject(err) }) }6.3 大文件上传问题
对于大文件上传,可以考虑以下优化:
- 分片上传
- 断点续传
- 上传进度显示
- 压缩文件后再上传
// 分片上传示例 async function chunkUpload(file, chunkSize = 2 * 1024 * 1024) { const chunkCount = Math.ceil(file.size / chunkSize) for (let i = 0; i < chunkCount; i++) { const start = i * chunkSize const end = Math.min(file.size, start + chunkSize) const chunk = file.slice(start, end) await uploadChunk({ chunk, index: i, total: chunkCount, fileHash: file.hash }) } await mergeChunks({ filename: file.name, fileHash: file.hash }) }7. TypeScript支持
对于使用TypeScript的项目,可以增强类型支持:
// src/types/api.d.ts declare interface ResponseData<T = any> { code: number data: T message: string } // 修改request.ts import type { AxiosRequestConfig, AxiosResponse } from 'axios' interface MyResponse<T> extends AxiosResponse { data: ResponseData<T> } export function request<T = any>(config: AxiosRequestConfig): Promise<T> { return service(config).then((res: MyResponse<T>) => res.data) } // 使用示例 interface UserInfo { name: string age: number } export function getUserInfo(): Promise<UserInfo> { return request({ url: '/user/info' }) }8. 测试与调试技巧
8.1 Mock数据方案
开发阶段可以使用Mock数据:
- 本地Mock:
// mock/user.js export default [ { url: '/api/user/login', method: 'post', response: ({ body }) => { if (body.username === 'admin') { return { code: 200, data: { token: 'mock-token' } } } return { code: 400, message: '账号或密码错误' } } } ]- 使用Mock.js:
import Mock from 'mockjs' Mock.mock('/api/user/info', 'get', { 'code': 200, 'data': { 'name': '@cname', 'age|20-60': 1, 'avatar': '@image("200x200")' } })8.2 调试技巧
在Chrome开发者工具的Network面板中:
- 查看请求/响应头信息
- 检查请求参数格式
- 分析响应时间线
使用axios的请求/响应日志:
// 请求拦截器 config.metadata = { startTime: new Date() } // 响应拦截器 response.config.metadata.endTime = new Date() response.duration = response.config.metadata.endTime - response.config.metadata.startTime console.log(`请求 ${response.config.url} 耗时 ${response.duration}ms`)- 使用代理工具(如Charles)抓包分析
9. 安全最佳实践
9.1 常见安全防护
- CSRF防护:
// 从cookie中读取csrf token function getCookie(name) { const value = `; ${document.cookie}` const parts = value.split(`; ${name}=`) if (parts.length === 2) return parts.pop().split(';').shift() } // 在请求头中添加 config.headers['X-CSRF-TOKEN'] = getCookie('csrfToken')- XSS防护:
- 对响应数据中的HTML内容进行转义
- 设置Content-Security-Policy头
- 敏感信息保护:
- 不要在前端存储敏感信息
- 使用HttpOnly cookie存储token
9.2 请求限流处理
对于频繁请求的接口,后端可能会限流,前端可以:
- 识别429状态码并提示用户
- 实现自动退避重试
function requestWithBackoff(config, retries = 3, delay = 1000) { return request(config).catch(err => { if (err.response?.status !== 429 || retries <= 0) { return Promise.reject(err) } return new Promise(resolve => { setTimeout(() => { resolve(requestWithBackoff(config, retries - 1, delay * 2)) }, delay) }) }) }10. 项目集成示例
10.1 与Vuex集成
在大型项目中,通常需要将API请求与状态管理结合:
// store/modules/user.js const actions = { login({ commit }, userInfo) { return new Promise((resolve, reject) => { login(userInfo).then(response => { const { token } = response commit('SET_TOKEN', token) setToken(token) resolve() }).catch(error => { reject(error) }) }) }, getInfo({ commit }) { return new Promise((resolve, reject) => { getInfo().then(response => { const { data } = response commit('SET_USER_INFO', data) resolve(data) }).catch(error => { reject(error) }) }) } }10.2 与组件集成
在组件中使用封装的API:
<script> import { getUserInfo } from '@/api/user' export default { data() { return { user: null, loading: false } }, methods: { async fetchUser() { this.loading = true try { this.user = await getUserInfo() } catch (error) { console.error('获取用户信息失败', error) } finally { this.loading = false } } }, created() { this.fetchUser() } } </script>11. 性能监控与统计
11.1 请求性能统计
可以收集请求性能数据用于分析:
// 在响应拦截器中 if (process.env.NODE_ENV === 'production') { const metrics = { url: response.config.url, method: response.config.method, status: response.status, duration: response.duration, timestamp: new Date().toISOString() } // 发送到监控系统 sendMetrics(metrics) }11.2 错误日志收集
前端错误日志收集方案:
function logError(error) { const errorInfo = { type: 'API_ERROR', time: new Date().toISOString(), url: error.config?.url, method: error.config?.method, status: error.response?.status, message: error.message, stack: error.stack } // 发送到日志服务 sendLog(errorInfo) } // 在全局错误处理中调用 service.interceptors.response.use(..., error => { logError(error) return handleHttpError(error) })12. 移动端适配方案
12.1 网络状态检测
在移动端需要特别关注网络状态:
// 检测网络状态 function checkNetwork() { return navigator.onLine ? Promise.resolve() : Promise.reject(new Error('网络不可用')) } // 在请求前检查 service.interceptors.request.use(async config => { await checkNetwork() return config })12.2 离线缓存策略
对于重要数据可以实现离线缓存:
// 简单的缓存策略 export function getDataWithCache(url) { const cacheKey = `cache_${url}` const cacheData = localStorage.getItem(cacheKey) if (cacheData) { try { const { data, timestamp } = JSON.parse(cacheData) // 1小时内使用缓存 if (Date.now() - timestamp < 3600000) { return Promise.resolve(data) } } catch (e) { localStorage.removeItem(cacheKey) } } return request({ url }).then(data => { localStorage.setItem(cacheKey, JSON.stringify({ data, timestamp: Date.now() })) return data }) }13. 微前端场景下的适配
在微前端架构中,需要特别注意:
- 避免全局Axios配置冲突:
// 每个微应用使用独立的实例 const service = axios.create()- 共享认证信息:
// 从主应用获取token const token = window.parent.getGlobalState().token- 接口前缀区分:
// 根据微应用名称添加前缀 const service = axios.create({ baseURL: `/micro-app/${appName}/api` })14. 服务端渲染(SSR)适配
在Nuxt.js等SSR框架中需要注意:
- 区分客户端和服务端请求:
let baseURL = '/api' if (process.server) { baseURL = 'http://backend-service/api' }- cookie传递处理:
// 服务端请求时手动传递cookie if (process.server && req.headers.cookie) { config.headers.Cookie = req.headers.cookie }- 避免服务端请求污染客户端:
// 在plugin中创建实例 export default function ({ $axios, req }) { if (process.server) { $axios.setBaseURL('http://backend-service') $axios.defaults.headers.common.Cookie = req.headers.cookie || '' } }15. 未来演进方向
随着前端技术的发展,网络请求处理也在不断演进:
- GraphQL集成:
- 替代传统RESTful API
- 精确获取所需数据
- 减少请求次数
- WebSocket实时通信:
- 与HTTP请求配合使用
- 处理实时数据更新
- 实现双向通信
- Service Worker离线缓存:
- 更强大的离线体验
- 后台同步功能
- 推送通知支持
- HTTP/2优化:
- 多路复用减少连接数
- 头部压缩提高效率
- 服务器推送功能
在实际项目中,我通常会根据团队技术栈和项目需求,选择最适合的封装方案。对于中小型项目,本文介绍的Axios封装已经足够应对大多数场景;对于大型复杂项目,可能需要考虑更高级的架构设计。