引言
每次在VSCode里看到项目中那些赤裸裸的axios请求调用,你是不是也和我一样浑身难受?就像穿着睡衣见客户------虽然能用,但总少了点专业感。今天,我要带你给axios穿上得体的"西装"!
想象一下这些日常开发中的糟心场景:
- 接口返回401时页面没有任何提示,用户操作突然卡死
- 每个请求都要手动加loading,代码里充斥着重复的ElLoading.service()
- 后端突然返回一个神秘错误码,前端一脸懵逼不知道该怎么提示用户
- 提交表单时手抖多点几次,结果数据库里冒出几条重复数据
作为一个经历过这些痛苦的老司机,我决定把我的"生存经验"分享给你。这不是那种教科书式的封装教程,而是一个真正从实战中摸爬滚打出来的解决方案。我们将用TypeScript+Vue3+Element Plus的组合,打造一个请求库:
正文
一、创建Axios实例:你的HTTP请求"管家"
在项目中直接使用 axios.get()
或 axios.post()
虽然方便,但就像租房不签合同------短期能用,长期必出问题。我们需要一个专属定制的Axios实例,来统一管理所有API请求的"家务事"。
1. 基础配置:给请求一个"家"
typescript
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // 环境变量配置的API根路径
timeout: 10000 // 10秒超时
})
baseURL
:通过环境变量动态配置,区分开发/生产环境,避免硬编码timeout
:10秒超时是经过验证的平衡值(短了影响大请求,长了用户以为卡死)
💡 Pro Tip :别在代码里写死
baseURL
!用import.meta.env
配合.env
文件管理,让不同环境自动切换地址。
2. 默认请求头:设置"着装规范"
typescript
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
- 统一设置
Content-Type
,避免后端收到undefined
时一脸懵逼 - 个别特殊请求(如文件上传)可以在具体请求中覆盖这个配置
3. 为什么不用默认实例?
直接修改axios.defaults
会影响全局,就像在合租房的公共区域乱堆杂物------其他第三方库的请求可能被意外影响。创建一个独立实例能保证:
- ✅ 隔离性:项目请求配置不会污染全局
- ✅ 可扩展性:后续可创建多个实例(如专门处理文件上传的实例)
- ✅ 可维护性:所有自定义配置集中管理
4. 环境变量安全提示
typescript
// .env.development
VITE_APP_BASE_API = '/api' // 开发环境代理
// .env.production
VITE_APP_BASE_API = 'https://api.your-domain.com' // 生产环境真实地址
- 使用
VITE_
前缀暴露变量(Vite特性) - 敏感信息不要前端配置,应该通过后端动态获取
关键点总结 :
🔹 创建独立实例避免全局污染
🔹 baseURL
通过环境变量动态注入
🔹 默认超时和请求头是基础保障
🔹 像对待租房合同一样认真对待Axios初始配置
在下一部分,我们将深入请求拦截器,看看如何给每个请求"化妆打扮"再出门------包括自动携带Token、防重复提交等高级技巧。
二、请求拦截器:给每个请求"化妆"再出门
如果说创建实例是给axios安了个家,那么请求拦截器就是每次出门前的精致打扮。让我们拆解这段"化妆术"的每个关键步骤:
1. Token管理:你的请求"身份证"
typescript
const isToken = (config.headers || {}).isToken === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken()
}
getToken()
:这是一个自定义函数(通常从localStorage/cookie读取)- 如何适配你的项目 :
- 如果不用JWT,改成你们团队的认证方式(如Basic Auth)
- 不需要认证的接口,在请求时添加
headers: { isToken: false }
- 为什么用
=== false
:
这是一个防御性编程技巧,确保只有当显式设置为false
时才跳过token,避免undefined
导致的意外行为
2. 防重复提交:防止用户"手抖"
typescript
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
// 计算请求唯一标识的字符串长度,用于避免缓存过大数据
const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小
const limitSize = 5 * 1024 * 1024 // 限制存放数据5M
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
return config
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url // 请求地址
const s_data = sessionObj.data // 请求数据
const s_time = sessionObj.time // 请求时间
const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交'
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
- 为什么需要防重复 :
- 用户双击提交按钮
- 网络延迟导致重复请求
- 可能引发数据库重复数据或支付重复扣款
Object.keys(JSON.stringify(requestObj)).length
:
计算请求体大小,防止大文件占用过多内存(5MB限制是经验值),浏览器 sessionStorage 通常限制为 5-10MB
3. GET请求参数处理
typescript
if (config.method === 'get' && config.params) {
config.url = config.url + '?' + tansParams(config.params) // tansParams是自定义参数序列化函数
}
- **
tansParams
**的作用:
将对象{ page: 1, size: 10 }
转成page=1&size=10
格式
(你的项目可能需要换成qs.stringify
或自定义逻辑)
4. 拦截器 vs 普通请求器
特性 | 拦截器 | 普通请求器 |
---|---|---|
执行时机 | 所有请求前后自动触发 | 手动调用时执行 |
使用场景 | 全局逻辑(如Token、Loading) | 具体业务请求 |
修改请求 | 可修改config | 只能传递初始参数 |
5. 关键问题解答
Q:为什么限制请求数据大小?
A:防止超大请求体导致:
- 内存溢出(OOM)
- 防重复提交的缓存失效
- 服务器拒绝处理(如Nginx默认限制1MB)
Q:如何在自己的项目实现防重复?
A:可以简化为:
typescript
// 简易版实现(基于URL+参数指纹)
const requestKey = `${config.url}_${JSON.stringify(config.data)}`
if (cache.session.get(requestKey)) {
return Promise.reject('请勿重复提交')
}
cache.session.set(requestKey, true, 1000) // 1秒后自动释放
Q:为什么用(config.headers || {})
?
A:防御config.headers
未定义的情况,等效于:
typescript
const headers = config.headers ? config.headers : {}
拦截器改造指南 :
🔸 替换getToken()
为你们项目的认证方式
🔸 根据后端API规范调整参数序列化逻辑(如时间格式处理)
🔸 防重复时间间隔(interval
)按业务需求调整
🔸 超大文件上传需要特殊处理(建议走单独实例)
在下一部分,我们将解剖响应拦截器------这里才是真正见证"奇迹"的地方,包括自动错误处理、Token过期跳转等核心逻辑。
三、响应拦截器:请求归来后的"质检流水线"
如果说请求拦截器是精心包装快递,那么响应拦截器就是拆包裹时的验货过程。这里藏着让前端优雅处理各种异常的终极秘密!
1. 二进制响应:特殊通道处理
typescript
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data // 直接放行二进制流
}
- 适用场景:Excel导出、PDF预览、图片下载等
- 为什么特殊处理:二进制数据不能被JSON解析,需要"绿色通道"
2. 状态码质检流水线
(1) 401过期处理:优雅重新登录
typescript
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true
ElMessageBox.confirm('登录过期,请重新登录', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消'
}).then(() => {
useUserStore().logOut().then(() => {
location.href = '/login?redirect=' + encodeURIComponent(route.fullPath)
})
})
}
return Promise.reject('会话过期')
}
- 关键设计 :
isRelogin
全局锁防止弹窗重复- 跳转登录页携带当前路由,登录后自动返回
- 比直接跳登录页体验提升100%
(2) 500服务器错误:精准定位
typescript
else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
}
- 为什么单独处理:服务器内部错误通常需要特殊记录
- msg来源 :优先使用预定义的
errorCode
,其次用后端返回消息
(3) 业务异常:601警告提示
typescript
else if (code === 601) {
ElMessage({ message: msg, type: 'warning' }) // 黄色温和提示
return Promise.reject(new Error(msg))
}
- 适用场景:余额不足、操作限制等业务异常
- 与500的区别:不需要红色恐慌提示
3. 网络异常处理:给用户看得懂的人话
typescript
error => {
let { message } = error
if (message == "Network Error") {
message = "网络连接异常,请检查网络"
} else if (message.includes("timeout")) {
message = "请求超时,请重试"
}
ElMessage({ message, type: 'error', duration: 5000 })
}
- 翻译策略 :
Network Error
→ 网络连接问题timeout
→ 请求超时status code
→ 具体接口异常
- 为什么重要:用户看不懂英文技术报错!
4. 核心问题解答
Q:blobValidate
是什么?
A:自定义的Blob校验函数,典型实现:
typescript
function blobValidate(data) {
return data instanceof Blob && data.size > 0
}
Q:为什么用Promise.reject传递错误?
A:保持错误冒泡能力,让业务代码可以try/catch
或.catch()
捕获
Q:如何扩展自定义错误码?
A:在errorCode.js
中添加映射,比如:
javascript
export default {
401: "未授权",
403: "禁止访问",
1001: "商品库存不足" // 业务自定义代码
}
5. 拦截器改造Checklist
- 🔹 根据业务补充错误码处理(如402支付相关)
- 🔹 国际化:将中文提示换成i18n多语言
- 🔹 监控埋点:在错误分支添加Sentry监控
- 🔹 重试机制:对网络错误可添加自动重试逻辑
响应拦截器设计精髓 :
✅ 分层处理:二进制、HTTP错误、业务错误各走不同通道
✅ 人话提示:把技术术语翻译成用户能理解的语言
✅ 可扩展性:通过errorCode轻松扩展新错误类型
✅ 体验优化:401处理保留返回路径,提升用户留存率
至此,一个企业级Axios封装的核心骨架已经完成。接下来你可以:
- 添加请求缓存功能
- 实现自动化Mock适配
- 集成TypeScript类型提示
- 添加性能监控埋点
最终效果:你的API调用会像这样优雅:
typescript
api.getUserInfo()
.then(data => { /* 业务处理 */ })
.catch(err => { /* 统一错误已处理,这里只处理特殊逻辑 */ })
最后再补充一下,有些人可能不是很了解config,我来给大家介绍一下config,config
是 axios 自带的配置对象,只要你引入了 axios,就能直接用,
是 axios 原生提供的核心配置对象 ,就像给每个 HTTP 请求准备的"定制化工具箱"。当你调用 axios(config)
或 axios.get(url, config)
时,这个对象决定了请求的所有行为。
** 原生 vs 自定义 Config**
类型 | 来源 | 示例参数 | 特点 |
---|---|---|---|
原生配置 | axios 内置 | url , method , timeout |
所有 axios 版本通用 |
自定义配置 | 项目/团队扩展 | isToken , repeatSubmit |
需要拦截器配合处理 |
3. 关键原生配置详解
这些是官方文档中最常用的配置项:
js
{
// 核心配置
url: '/user', // 请求路径(必填)
method: 'get', // HTTP方法(默认get)
baseURL: '/api', // 自动拼接到url前
// 数据相关
params: { id: 1 }, // GET参数(拼接到URL)
data: { name: 'Jack' },// POST请求体
// 网络控制
timeout: 5000, // 超时时间(ms)
responseType: 'json', // 响应格式(json/blob等)
// 浏览器相关
withCredentials: true, // 跨域携带cookie
xsrfCookieName: 'XSRF-TOKEN' // CSRF防护
}
总结
一句话总结:
这个axios封装通过拦截器自动处理了token、错误提示、防重复提交,让你用起来像点外卖一样简单------只管说要什么(发请求),剩下的包装配送(鉴权/loading/错误处理)全自动搞定!
三要素:
- 省心 - 自带Token管理,无需手动处理
- 安全 - 自动防重复提交,防止手抖连点
- 友好 - 统一错误提示,用户看到白话文报错
调用示例:
js
// 只要关心业务参数,其他全托管
request.post('/order', { id: 123 })
就像外卖APP下单,你只管选菜品,平台负责配送/售后所有脏活累活 🚀。
本封装的核心逻辑(axios实例、拦截器)是框架无关的。若用于React或原生JS项目,只需将其中的 ElMessage
替换为对应的UI组件(如Ant Design的 message
),并将 useUserStore
替换为你的状态管理逻辑即可。