1. axios 统一封装的整体结构设计及优势
核心结构与模块职责
axios 封装通常采用 "分层解耦" 设计,核心模块及职责如下:
- 基础配置模块:定义全局默认配置,如baseURL(区分环境)、timeout(默认 5000ms)、responseType: 'json',以及请求头默认值(如Content-Type: application/json),通过axios.create()创建实例,避免污染全局 axios 对象。
- 拦截器模块:包含请求拦截器和响应拦截器,集中处理通用逻辑(如参数添加、错误捕获),独立于业务请求,便于统一维护。
- 工具函数模块:封装辅助方法,如getToken()(从 localStorage/cookie 获取 token)、formatParams()(处理 GET 请求参数拼接、POST 请求参数序列化)、handleBlob()(文件流处理),避免重复代码。
- 请求方法封装模块:对外暴露get、post、download等方法,接收 "业务参数 + 自定义配置",内部整合基础配置与拦截器,简化业务方调用。
- 错误处理模块:统一管理请求错误(网络错误、超时、后端错误码),提供错误提示、重试、跳转等标准化处理方案。
结构优势
- 扩展性:新增功能(如请求缓存)时,只需在对应模块(如工具函数 / 拦截器)添加逻辑,无需修改所有业务请求;切换环境(如测试→生产)时,仅需修改基础配置的baseURL。
- 维护性:通用逻辑(如 token 携带)集中在拦截器,若后端调整参数名(如token→authToken),仅需修改拦截器中 1 处代码,无需逐个修改业务接口。
- 易用性:业务方调用时无需关注baseURL、拦截器等细节,只需传入 "接口路径 + 参数",降低使用成本(如request.get('/user/list', { page: 1 }))。
2. 两类公用参数的实现方式及特殊场景处理
(1)固定公用参数(如appId、token):请求拦截器全局添加
实现逻辑:在请求拦截器中,对所有请求的params(GET)或data(POST)添加固定参数,同时处理token过期的特殊情况(如 token 不存在时跳转登录)。
javascript
// 请求拦截器示例
axiosInstance.interceptors.request.use(
(config) => {
const token = getToken(); // 自定义工具函数:从localStorage获取token
// 1. 处理GET参数(params)
config.params = {
appId: 'com.xxx.frontend', // 固定参数
token: token || '', // token存在则携带,否则传空
...config.params, // 业务参数覆盖默认(若有同名参数)
};
// 2. 处理POST参数(data)
if (config.method === 'post' && config.data) {
config.data = {
appId: 'com.xxx.frontend',
token: token || '',
...config.data,
};
}
return config;
},
(error) => Promise.reject(error)
);
适用场景:所有接口必须携带的参数,且参数值相对固定(如appId固定,token仅随用户登录状态变化)。
(2)动态公用参数(如分页pageSize):请求方法默认值 + 业务方可选覆盖
实现逻辑:在封装的get/post方法中,为参数设置默认值,业务方若需修改可手动传入,无需全局强制添加。
csharp
// 封装get方法示例
export const get = (url, params = {}, config = {}) => {
// 动态公用参数:分页默认pageSize=10,page=1
const defaultPageParams = {
pageSize: 10,
page: 1,
};
// 合并默认分页参数与业务参数(业务参数优先级更高)
const finalParams = { ...defaultPageParams, ...params };
return axiosInstance.get(url, {
params: finalParams,
...config, // 自定义配置(如禁用loading)
});
};
适用场景:部分接口(如列表查询)需要的通用参数,且参数值可能随业务场景变化(如 A 列表pageSize=20,B 列表pageSize=10)。
(3)排除特定公用参数的处理:自定义配置标记 + 拦截器过滤
业务方在请求时通过config传入excludeParams(需排除的参数名数组),拦截器中根据该标记过滤参数:
javascript
// 业务方调用示例(排除token参数)
get('/user/info', { id: 123 }, { excludeParams: ['token'] });
// 拦截器中添加过滤逻辑
axiosInstance.interceptors.request.use(
(config) => {
const { excludeParams = [] } = config;
// 过滤GET参数
config.params = Object.fromEntries(
Object.entries({
appId: 'com.xxx.frontend',
token: getToken() || '',
...config.params,
}).filter(([key]) => !excludeParams.includes(key))
);
// 过滤POST参数逻辑类似...
return config;
},
(error) => Promise.reject(error)
);
3. 请求 / 响应拦截器的常见逻辑与实现
(1)请求拦截器:除公用参数外的核心逻辑
- 请求头设置:根据请求类型(如文件上传)动态修改Content-Type,避免手动配置错误。
arduino
if (config.headers['Content-Type'] === 'multipart/form-data') {
// 文件上传:无需序列化,axios自动处理FormData
delete config.headers['Content-Type']; // 清除默认json头,避免冲突
} else if (config.method === 'post' && !config.headers['Content-Type']) {
// 默认POST请求:设置json头
config.headers['Content-Type'] = 'application/json';
}
- token 过期前刷新:通过tokenExpireTime(登录时后端返回)判断 token 剩余时间,若 < 5 分钟则发起刷新请求,避免请求因 token 过期失败。
ini
const tokenExpireTime = localStorage.getItem('tokenExpireTime');
const now = Date.now();
// 若token存在且即将过期,且无正在进行的刷新请求
if (token && tokenExpireTime - now < 5 * 60 * 1000 && !isRefreshing) {
isRefreshing = true;
// 发起刷新token请求
refreshToken().then((newToken) => {
localStorage.setItem('token', newToken);
isRefreshing = false;
}).catch(() => {
// 刷新失败:跳转登录
router.push('/login');
isRefreshing = false;
});
}
- 请求参数格式转换:处理 GET 请求数组参数(如ids=[1,2]→ids=1&ids=2),避免后端解析错误。
javascript
// 工具函数:格式化GET数组参数
const formatGetParams = (params) => {
return Object.fromEntries(
Object.entries(params).flatMap(([key, value]) => {
if (Array.isArray(value)) {
return value.map((v) => [key, v]); // 数组参数拆分为多个key-value
}
return [[key, value]];
})
);
};
// 拦截器中调用
config.params = formatGetParams(config.params);
(2)响应拦截器:统一处理响应结果与错误
- 正常响应处理:提取后端返回的data(通常后端响应格式为{ code: 200, data: {}, message: '' }),直接返回data给业务方,简化调用。
javascript
axiosInstance.interceptors.response.use(
(response) => {
const { code, data, message } = response.data;
// 后端自定义成功码(如200)
if (code === 200) {
return data; // 业务方直接获取data,无需处理外层结构
} else {
// 非成功码:触发错误处理(如提示"参数错误")
ElMessage.error(message || '请求失败');
return Promise.reject(new Error(message || '响应错误'));
}
},
(error) => {
// 网络错误/超时处理
if (!error.response) {
if (error.message.includes('timeout')) {
ElMessage.error('请求超时,请稍后重试');
} else {
ElMessage.error('网络异常,请检查网络连接');
}
return Promise.reject(error);
}
// 后端返回错误码处理
const { status, data } = error.response;
switch (status) {
case 401:
// 未授权:清除token并跳转登录
localStorage.removeItem('token');
router.push('/login?redirect=' + window.location.pathname);
ElMessage.error(data.message || '登录已过期,请重新登录');
break;
case 403:
// 权限不足:提示并返回上一页
ElMessage.error('您无此操作权限');
router.go(-1);
break;
case 500:
// 服务器错误:提示并上报错误日志
ElMessage.error('服务器内部错误,请联系管理员');
reportError(error); // 自定义日志上报函数
break;
default:
ElMessage.error(data.message || `请求错误(${status})`);
}
return Promise.reject(error);
}
);
4. 全局 loading 自动化配置逻辑
(1)判断请求状态:计数器 + 请求状态管理
通过 "请求计数器" 记录当前活跃请求数,计数器 > 0 时显示 loading,=0 时隐藏 loading,避免单个请求的 loading 闪烁:
ini
let requestCount = 0; // 请求计数器
const showLoading = () => {
if (requestCount === 0) {
// 使用Element Plus的Loading组件,全局遮罩
ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.5)',
});
}
requestCount++;
};
const hideLoading = () => {
requestCount--;
if (requestCount === 0) {
// 关闭loading(需确保获取到Loading实例)
const loadingInstance = ElLoading.service();
loadingInstance.close();
}
};
- 调用时机:请求拦截器中调用showLoading(),响应拦截器(成功 / 失败)中调用hideLoading(),确保每个请求对应 "显示→隐藏" 的完整流程。
(2)单个请求禁用 loading:自定义配置标记
封装请求方法时,允许业务方通过config传入showLoading: false,拦截器中根据该标记决定是否执行 loading 逻辑:
javascript
// 1. 请求拦截器中添加判断
axiosInstance.interceptors.request.use(
(config) => {
// 若未禁用loading,则显示
if (config.showLoading !== false) {
showLoading();
}
// ...其他逻辑
return config;
},
(error) => {
// 请求发送失败时,若未禁用loading,需隐藏
if (error.config?.showLoading !== false) {
hideLoading();
}
return Promise.reject(error);
}
);
// 2. 业务方调用示例(下拉刷新请求禁用loading)
get('/user/list', { page: 2 }, { showLoading: false });
(3)多请求同步 loading:依赖计数器机制
由于使用requestCount计数器,多个请求同时发起时,每个请求会使计数器 + 1,只有所有请求完成(计数器归 0)才会隐藏 loading,天然避免 "先隐藏再显示":
- 例:同时发起 A、B 两个请求→requestCount从 0→1→2(显示 loading)→A 完成→1→B 完成→0(隐藏 loading)。
5. 统一文件下载处理(文件流 + 不同类型文件)
(1)axios 请求配置:确保正确接收文件流
- 设置responseType: 'blob':告知 axios 将响应数据解析为 Blob 对象(文件流),而非默认的 JSON,避免文件流被错误解析导致乱码。
- 设置responseEncoding: 'utf-8'(可选):确保中文文件名正常解析。
javascript
// 封装文件下载方法
export const download = (url, params = {}, config = {}) => {
return axiosInstance.get(url, {
params,
responseType: 'blob', // 核心配置:接收文件流
responseEncoding: 'utf-8',
...config,
}).then((response) => {
handleDownloadResponse(response); // 处理文件流
});
};
(2)文件流处理:区分下载与预览
- 核心逻辑:从响应头content-disposition中提取文件名,通过URL.createObjectURL(blob)创建临时 URL,结合a标签(下载)或iframe(预览)实现功能。
- 处理中文文件名乱码:通过decodeURIComponent()解码头信息中的文件名(后端需将文件名编码为 UTF-8)。
javascript
// 处理下载响应(通用方法)
const handleDownloadResponse = (response) => {
const blob = response.data;
const headers = response.headers;
// 1. 提取文件名(从content-disposition头)
const disposition = headers['content-disposition'] || '';
const filenameMatch = disposition.match(/filename=([^;]+)/i);
// 解码中文文件名(避免乱码)
const filename = filenameMatch
? decodeURIComponent(filenameMatch[1].replace(/"/g, ''))
: 'default_file';
// 2. 判断文件类型:预览(PDF)或下载(Excel/Word等)
const blobType = blob.type;
const url = URL.createObjectURL(blob); // 创建临时URL
if (blobType.includes('pdf')) {
// PDF预览:使用iframe
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.width = '100%';
iframe.style.height = '800px';
iframe.style.border = 'none';
// 替换页面中预览容器的内容
const previewContainer = document.getElementById('pdf-preview');
if (previewContainer) {
previewContainer.innerHTML = '';
previewContainer.appendChild(iframe);
}
} else {
// 其他文件:下载(a标签)
const a = document.createElement('a');
a.href = url;
a.download = filename; // 设置下载文件名
// 触发点击(解决部分浏览器不支持直接点击)
document.body.appendChild(a);
a.click();
// 清理临时资源
document.body.removeChild(a);
URL.revokeObjectURL(url); // 释放URL对象,避免内存泄漏
}
};
6. 统一错误处理与业务方自定义错误逻辑
(1)请求过程中的常见错误类型及统一处理
错误类型 | 触发场景 | 统一处理逻辑 |
---|---|---|
网络错误 | 断网、服务器宕机 | 提示 "网络异常,请检查连接",不上报日志(用户环境问题) |
请求超时 | timeout时间内未收到响应(如默认 5000ms) | 提示 "请求超时,请稍后重试",可配置重试 1 次(偶发网络波动) |
401 未授权 | token 过期、未登录 | 清除 token→跳转登录页(带当前页面地址,登录后返回)→提示 "登录已过期" |
403 权限不足 | 无接口访问权限(如普通用户调用管理员接口) | 提示 "无操作权限"→返回上一页,不上报日志(业务权限问题) |
404 接口不存在 | 接口路径错误、环境配置错误 | 提示 "请求资源不存在"→上报日志(开发配置问题) |
500/504 服务器错误 | 后端代码异常、网关超时 | 提示 "服务器内部错误,请联系管理员"→上报详细日志(后端问题) |
后端自定义错误(如 400) | 参数错误、业务逻辑错误(如 "手机号已存在") | 提示后端返回的message(如 "手机号已存在"),不上报日志(业务场景错误) |
统一处理代码 :参考 "响应拦截器" 部分的错误处理逻辑,通过switch (status)和 code
(2)业务方自定义错误处理逻辑:支持catch捕获与配置回调
全局错误处理覆盖通用场景,但部分业务需自定义逻辑(如登录失败后不跳转,而是显示验证码),可通过两种方式实现:
- 方式 1:业务方在请求 catch 中重写处理
由于拦截器中已将错误Promise.reject,业务方可直接在catch中捕获错误,覆盖全局逻辑:
typescript
// 业务方调用示例:自定义401错误处理(不跳转登录,显示弹窗)
login({ username: 'test', pwd: '123' })
.then((res) => { /* 成功逻辑 */ })
.catch((error) => {
const status = error.response?.status;
if (status === 401) {
// 自定义处理:显示验证码弹窗,而非跳转登录
showVerifyCodeModal();
} else {
// 非401错误:沿用全局处理(如提示网络错误)
ElMessage.error(error.message || '请求失败');
}
});
- 方式 2:封装时提供 errorCallback 配置
在请求方法中增加errorCallback参数,允许业务方传入自定义回调,拦截器中优先执行回调:
typescript
// 1. 封装post方法:增加errorCallback参数
export const post = (url, data = {}, config = {}) => {
const { errorCallback, ...restConfig } = config;
return axiosInstance.post(url, data, restConfig)
.catch((error) => {
// 若业务方传入回调,优先执行
if (typeof errorCallback === 'function') {
return errorCallback(error);
}
// 无回调:沿用全局错误处理
return Promise.reject(error);
});
};
// 2. 业务方调用:传入errorCallback
post(
'/login',
{ username: 'test', pwd: '123' },
{
errorCallback: (error) => {
if (error.response?.status === 401) {
showVerifyCodeModal(); // 自定义处理
}
}
}
);
7. 多环境baseURL动态切换与代理配置
(1)baseURL动态切换方案:环境变量 + 配置文件
通过 "环境变量区分环境"+"配置文件存储baseURL" 实现切换,避免硬编码修改,常见方案如下:
- 步骤 1:创建环境配置文件
在项目src/config目录下创建多环境配置文件,存储不同环境的baseURL:
arduino
// src/config/env.js
export default {
// 开发环境(本地)
development: {
baseURL: '/api' // 配合代理,避免跨域
},
// 测试环境(测试服)
test: {
baseURL: 'https://test.xxx.com/api'
},
// 生产环境(正式服)
production: {
baseURL: 'https://prod.xxx.com/api'
}
};
- 步骤 2:通过环境变量判断当前环境
利用构建工具(如 Vite、Webpack)的环境变量,自动识别当前环境(如NODE_ENV):
javascript
// src/utils/request.js(axios封装文件)
import envConfig from '@/config/env';
// 1. 获取当前环境(Vite中用import.meta.env.MODE,Webpack中用process.env.NODE_ENV)
const currentEnv = import.meta.env.MODE || 'development';
// 2. 创建axios实例:动态设置baseURL
const axiosInstance = axios.create({
baseURL: envConfig[currentEnv].baseURL,
timeout: 5000
});
- 步骤 3:手动切换环境(可选)
若需支持用户手动切换环境(如测试人员切换测试 / 预发布环境),可通过 "本地存储 + 重新初始化实例" 实现:
ini
// 1. 手动切换环境的函数
export const switchEnv = (targetEnv) => {
// 存储环境到localStorage
localStorage.setItem('currentEnv', targetEnv);
// 重新初始化axios实例:更新baseURL
axiosInstance.defaults.baseURL = envConfig[targetEnv].baseURL;
};
// 2. 初始化时优先读取localStorage中的环境
const initAxios = () => {
const savedEnv = localStorage.getItem('currentEnv') || currentEnv;
axiosInstance.defaults.baseURL = envConfig[savedEnv].baseURL;
};
initAxios(); // 项目启动时初始化
(2)开发环境代理配置:避免跨域
开发环境中,baseURL设为/api,通过构建工具配置代理,将/api前缀的请求转发到真实后端地址,解决跨域问题:
- Vite 代理配置(vite.config.js)
javascript
export default defineConfig({
server: {
proxy: {
// 匹配所有以/api开头的请求
'/api': {
target: 'https://dev.xxx.com', // 后端开发环境地址
changeOrigin: true, // 开启跨域(修改请求头中的Origin)
rewrite: (path) => path.replace(/^/api/, '') // 移除/api前缀(若后端无该前缀)
}
}
}
});
- Webpack 代理配置(vue.config.js,Vue2 项目)
css
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://dev.xxx.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
};
(3)代理配置注意事项
- changeOrigin: true 必须开启 :若不开启,后端会收到Origin: localhost:3000(前端地址),可能因跨域策略拒绝请求;开启后,Origin会改为target地址(如dev.xxx.com)。
- rewrite/pathRewrite 适配后端前缀:若后端接口无/api前缀(如真实接口是/login,而非/api/login),需通过rewrite移除前缀,避免 404。
- 避免代理冲突:若项目中存在本地 Mock 服务(如/mock前缀),需确保代理规则不覆盖 Mock 请求(如仅代理/api前缀)。
8. 请求取消功能实现(CancelToken/AbortController)
axios 支持两种取消请求方式:CancelToken(旧版,axios < 0.22.0)和AbortController(新版,axios ≥ 0.22.0),以下以新版AbortController为例,实现请求取消与管理。
(1)核心实现:封装取消令牌管理
通过 "Map 存储取消控制器"+"请求标识关联控制器",实现单个 / 批量取消请求:
- 步骤 1:创建取消令牌管理工具
dart
// src/utils/requestCancel.js
// 存储取消控制器:key=请求标识(如url+method),value=AbortController实例
const controllerMap = new Map();
// 1. 创建取消控制器并存储
export const createCancelController = (requestKey) => {
// 若已有相同请求的控制器,先取消旧请求(避免重复请求)
if (controllerMap.has(requestKey)) {
cancelRequest(requestKey);
}
const controller = new AbortController();
controllerMap.set(requestKey, controller);
return controller.signal; // 返回signal,传入axios请求配置
};
// 2. 取消指定请求
export const cancelRequest = (requestKey) => {
const controller = controllerMap.get(requestKey);
if (controller) {
controller.abort(); // 触发取消
controllerMap.delete(requestKey); // 移除控制器
}
};
// 3. 取消所有未完成的请求(如页面跳转时)
export const cancelAllRequests = () => {
controllerMap.forEach((controller) => controller.abort());
controllerMap.clear();
};
- 步骤 2:集成到 axios 封装中
在请求方法中生成requestKey(如url+method),创建signal并传入 axios 配置:
javascript
// src/utils/request.js
import { createCancelController, cancelRequest } from './requestCancel';
export const get = (url, params = {}, config = {}) => {
// 生成请求标识:url + 'GET'(避免GET/POST同url冲突)
const requestKey = `${url}-GET`;
// 创建signal,传入axios配置
const signal = createCancelController(requestKey);
// 合并配置:添加signal
const finalConfig = { signal, ...config };
return axiosInstance.get(url, { params, ...finalConfig })
.finally(() => {
// 请求完成(成功/失败):移除控制器
cancelRequest(requestKey);
});
};
(2)业务方触发取消请求
- 场景 1:取消单个请求(如快速点击按钮)
业务方需存储requestKey,调用cancelRequest取消:
ini
// 业务组件中:取消"获取用户列表"请求
const cancelUserListRequest = () => {
const requestKey = '/user/list-GET'; // 与封装时的requestKey一致
cancelRequest(requestKey);
};
// 点击"取消"按钮触发
<button @click="cancelUserListRequest">取消加载</button>
- 场景 2:页面跳转时取消所有请求
在路由守卫中调用cancelAllRequests,避免页面跳转后请求仍占用资源:
javascript
// src/router/index.js
import { cancelAllRequests } from '@/utils/requestCancel';
router.beforeEach((to, from, next) => {
// 离开当前页面时,取消所有未完成的请求
cancelAllRequests();
next();
});
(3)注意事项
- AbortController 兼容性:axios ≥ 0.22.0 才支持,若项目使用旧版 axios,需改用CancelToken(逻辑类似,替换为CancelToken.source())。
- 避免重复取消:请求完成后需调用cancelRequest移除控制器,避免重复调用abort()导致错误。
- 错误捕获:取消请求会触发Cancel错误,需在拦截器中过滤,避免错误提示:
javascript
// 响应拦截器中过滤取消错误
axiosInstance.interceptors.response.use(
(res) => res,
(error) => {
// 取消请求的错误:不提示,直接返回
if (axios.isCancel(error)) {
console.log('请求已取消', error.message);
return Promise.reject(new Error('请求已取消'));
}
// 其他错误:正常处理
return Promise.reject(error);
}
);
9. 请求函数参数设计与配置优先级
(1)参数设计:兼顾易用性与灵活性
封装的请求函数(get/post/download)需支持 "基础参数 + 自定义配置",参数结构设计如下(以post为例):
typescript
/**
* POST请求封装
* @param {string} url - 接口路径(如'/login')
* @param {Object} data - 请求体参数(POST/PUT等)
* @param {Object} config - 自定义配置(覆盖全局配置)
* @param {number} config.timeout - 超时时间(覆盖全局5000ms)
* @param {Object} config.headers - 请求头(覆盖全局headers)
* @param {boolean} config.showLoading - 是否显示loading(默认true)
* @param {Function} config.errorCallback - 自定义错误回调
* @returns {Promise} - 请求结果Promise
*/
export const post = (url, data = {}, config = {}) => {
// 解构自定义配置,默认值与全局配置对齐
const {
timeout = axiosInstance.defaults.timeout, // 全局默认超时5000ms
headers = {},
showLoading = true,
errorCallback,
...restConfig
} = config;
// 合并配置:自定义配置覆盖全局
const finalConfig = {
timeout,
headers: { ...axiosInstance.defaults.headers, ...headers }, // headers合并
showLoading,
errorCallback,
...restConfig
};
return axiosInstance.post(url, data, finalConfig)
.catch((error) => {
if (errorCallback) return errorCallback(error);
return Promise.reject(error);
});
};
(2)配置优先级:业务自定义 > 全局默认
配置优先级从高到低为:业务方传入的自定义配置 → 全局默认配置 → axios 默认配置,具体规则如下:
- timeout:业务方传入config.timeout=10000 → 覆盖全局timeout=5000 → 覆盖 axios 默认timeout=0(无超时)。
- headers:采用 "浅合并",业务方传入的headers字段覆盖全局,未传入字段沿用全局:
css
// 全局headers:{ 'Content-Type': 'application/json', 'appId': '123' }
// 业务方headers:{ 'Content-Type': 'multipart/form-data' }
// 合并后:{ 'Content-Type': 'multipart/form-data', 'appId': '123' }
- showLoading:业务方传入showLoading=false → 覆盖全局默认true,不显示 loading。
- responseType:文件下载时传入responseType: 'blob' → 覆盖全局responseType: 'json'。
(3)业务方调用示例:灵活配置
go
// 示例1:自定义超时+禁用loading
post(
'/upload/file',
formData, // FormData格式(文件上传)
{
timeout: 30000, // 上传文件超时30s
showLoading: false, // 禁用loading
headers: { 'Content-Type': 'multipart/form-data' } // 覆盖Content-Type
}
);
// 示例2:自定义错误回调+修改baseURL
get(
'/user/info',
{ id: 123 },
{
baseURL: 'https://special.xxx.com/api', // 临时使用特殊baseURL
errorCallback: (error) => {
if (error.response?.status === 404) {
ElMessage.error('用户不存在');
}
}
}
);
10. 性能优化与用户体验增强功能(以 "请求重试" 为例)
(1)优化点选择:请求重试机制
针对 "偶发网络波动导致的请求失败"(如 502 网关错误、超时),自动重试 1-2 次,避免用户手动刷新,提升体验。
(2)实现思路
- 核心逻辑:在响应拦截器中判断错误类型(仅重试偶发错误),记录重试次数,达到最大次数后停止重试,返回错误。
- 关键配置:全局设置 "最大重试次数(如 2 次)" 和 "重试间隔(如 1000ms,避免频繁重试压垮服务器)"。
(3)核心代码实现
javascript
// src/utils/request.js
// 全局重试配置
const retryConfig = {
maxRetryCount: 2, // 最大重试次数
retryDelay: 1000, // 重试间隔(ms)
};
// 封装重试函数
const retryRequest = async (config, retryCount = 0) => {
try {
// 克隆请求配置:避免修改原配置
const cloneConfig = { ...config };
// 增加重试次数标识
cloneConfig._retryCount = retryCount;
// 发起重试请求
return await axiosInstance(cloneConfig);
} catch (error) {
// 重试次数达到上限:返回错误
if (retryCount >= retryConfig.maxRetryCount) {
return Promise.reject(error);
}
// 未达上限:延迟后重试(递归调用)
return new Promise((resolve) => {
setTimeout(() => {
resolve(retryRequest(config, retryCount + 1));
}, retryConfig.retryDelay);
});
}
};
// 响应拦截器中集成重试逻辑
axiosInstance.interceptors.response.use ((response) => response,
async (error) => {
// 1. 过滤无需重试的错误(取消请求、401 未授权、403 权限不足等)
const shouldRetry = !axios.isCancel (error) &&
error.response?.status !== 401 &&
error.response?.status !== 403 &&
error.response?.status !== 404 &&
(error.response?.status >= 500 || error.message.includes ('timeout'));
// 2. 若无需重试:直接返回错误
if (!shouldRetry) {
return Promise.reject (error);
}
// 3. 若需重试:获取原请求配置,调用重试函数
const originalConfig = error.config;
// 避免重复添加_retryCount(首次重试时无该字段)
const currentRetryCount = originalConfig._retryCount || 0;
// 4. 判断是否达到最大重试次数
if (currentRetryCount>= retryConfig.maxRetryCount) {
ElMessage.error (请求失败(已重试${currentRetryCount}次),请稍后再试);
return Promise.reject(error);
}
// 5. 发起重试
console.log (请求重试中(第${currentRetryCount + 1}次):${originalConfig.url});
return retryRequest(originalConfig, currentRetryCount);
});
(4)请求重试的收益与注意事项
- 核心收益:
-
提升用户体验:偶发的502网关错误、网络波动导致的超时,无需用户手动刷新页面,自动重试即可恢复,减少用户操作成本。
-
降低错误率:针对非业务性错误(如服务器临时过载),重试机制可有效降低最终的请求失败率,提升系统稳定性。
- 注意事项:
-
明确重试范围:仅对"服务器端偶发错误"(500+状态码、超时)重试,避免对"客户端错误"(401/403/404)重试,否则会无效请求浪费资源。
-
设置重试间隔 :通过
retryDelay=1000ms
避免短时间内频繁重试,防止加重服务器负担(如服务器过载时,频繁重试会加剧问题)。 -
避免幂等性问题:对"非幂等请求"(如POST提交表单、创建订单),需谨慎使用重试,避免重复创建数据(可通过后端接口幂等设计解决,如添加唯一请求ID)。
-
业务方自定义控制 :可在请求配置中增加
enableRetry: false
,允许业务方禁用特定请求的重试(如支付接口,避免重复扣款):
javascript
// 业务方调用:禁用重试(支付接口)
post('/order/pay', { orderId: 123 }, { enableRetry: false });
// 响应拦截器中增加判断:若disableRetry则不重试
const shouldRetry = !originalConfig.enableRetry === false && ...;