UniApp 小程序 - Token 过期自动刷新+失败请求缓存重试方案
·
UniApp 小程序 - Token 过期自动刷新+失败请求缓存重试方案
一、方案概述
该方案实现了 Token 过期统一拦截、新 Token 仅请求一次、失败请求缓存后重试 的核心功能,解决了多请求并发下 Token 过期导致的重复刷新、请求失败问题,提升了小程序的用户体验和接口调用稳定性。
核心特性
- 拦截接口返回的 Token 过期状态(
code = -1),统一处理刷新逻辑。 - 并发请求下,新 Token 仅获取一次,避免重复调用刷新接口。
- 缓存 Token 过期期间的所有失败请求,获取新 Token 后自动重试。
- 支持异常处理,刷新失败时清空请求队列并给出友好提示。
二、完整代码实现
import { API_CONFIG } from './api.config.js';
import { BASE_URL } from './config.js';
import { getToken, setStorage, Toast, hideLoading } from './tool.js';
import { wxLogin } from './api.public.js'; // 获取微信登录code
// 全局变量:存储刷新Token的Promise(确保仅请求一次)
let refreshPromise = null;
// 全局变量:缓存Token过期期间的失败请求队列
let requestQueue = [];
/**
* 自定义封装的请求方法
* @param {Object} options - 请求配置参数
* @param {Boolean} skipRefreshLock - 是否跳过刷新锁(刷新Token时的内部请求需设为true,避免循环拦截)
* @returns {Promise} - 请求结果Promise
*/
const myHttp = (options, skipRefreshLock = false) => {
return new Promise((resolve, reject) => {
// 若正在刷新Token且非内部刷新请求,将请求加入缓存队列
if (!skipRefreshLock && refreshPromise) {
requestQueue.push({
options,
resolve,
reject
});
} else {
// 直接发起请求
uni.request(createRequest(options, resolve, reject, skipRefreshLock));
}
});
};
/**
* 构建uni.request所需的配置参数
* @param {Object} options - 原始请求配置
* @param {Function} resolve - Promise成功回调
* @param {Function} reject - Promise失败回调
* @param {Boolean} skipRefreshLock - 是否跳过刷新锁
* @returns {Object} - 处理后的uni.request配置
*/
const createRequest = (options, resolve, reject, skipRefreshLock) => {
const param = dealRequestParams(options);
return {
...param,
success: (res) => {
// 接口状态码200(网络请求成功)
if (res.statusCode == 200) {
// Token过期(业务状态码-1)
if (res.data.code == -1) {
handleTokenExpiration(options, resolve, reject, skipRefreshLock);
} else {
// 正常返回,执行成功回调
resolve(res.data);
hideLoading();
}
} else {
// 处理HTTP状态码错误(401、404等)
handleHttpError(res.statusCode, reject);
}
},
fail: (err) => {
// 网络请求失败(如断网、超时)
Toast('网络请求出错!');
hideLoading();
reject(err);
}
};
};
/**
* 处理Token过期逻辑
* @param {Object} options - 原始请求配置
* @param {Function} resolve - Promise成功回调
* @param {Function} reject - Promise失败回调
* @param {Boolean} skipRefreshLock - 是否跳过刷新锁
*/
const handleTokenExpiration = (options, resolve, reject, skipRefreshLock) => {
// 将当前过期请求加入缓存队列
requestQueue.push({
options,
resolve,
reject
});
// 若未在刷新Token,发起刷新请求(确保仅执行一次)
if (!refreshPromise) {
refreshPromise = new Promise(async (refreshResolve) => {
try {
// 1. 获取微信登录code
const wxcode = await wxLogin();
// 2. 调用静默登录接口刷新Token(内部请求,skipRefreshLock=true避免循环拦截)
const refreshRes = await myHttp({
api: 'silentLogin',
method: 'post',
data: {
code: wxcode
}
}, true);
// 3. 刷新Token成功(业务状态码1且包含新Token)
if (refreshRes.code == 1 && refreshRes.data.token) {
const newToken = refreshRes.data.token;
// 4. 缓存新Token到本地
await setStorage('TOKEN', newToken);
// 5. 重试所有缓存的失败请求
retryQueuedRequests(newToken);
} else {
// 6. 刷新Token失败(如code无效、返回无Token)
clearRequestQueue('登录状态已失效,请重新登录');
}
} catch (error) {
// 7. 捕获刷新过程中的异常(如接口请求失败)
clearRequestQueue('Token 刷新失败,请检查网络');
} finally {
// 8. 重置刷新Promise,释放锁,允许后续可能的刷新操作
refreshPromise = null;
refreshResolve();
}
});
}
};
/**
* 重试所有缓存的失败请求
* @param {String} newToken - 新获取的有效Token
*/
const retryQueuedRequests = (newToken) => {
// 复制请求队列并清空原队列(避免重试过程中新增请求干扰)
const queue = [...requestQueue];
requestQueue = [];
// 遍历队列,逐个重试请求
queue.forEach(({ options, resolve, reject }) => {
// 构建新的请求配置,替换为新Token
const newOptions = {
...options,
header: {
...options.header,
token: newToken
}
};
// 重试请求,结果透传原Promise的回调
myHttp(newOptions, true).then(resolve).catch(reject);
});
};
/**
* 清空请求队列并返回错误信息
* @param {String} errorMessage - 错误提示信息
*/
const clearRequestQueue = (errorMessage) => {
// 复制请求队列并清空原队列
const queue = [...requestQueue];
requestQueue = [];
// 遍历队列,逐个执行失败回调
queue.forEach(({ reject }) => {
reject(new Error(errorMessage));
});
};
/**
* 处理HTTP状态码错误
* @param {Number} statusCode - HTTP状态码
* @param {Function} reject - Promise失败回调
*/
const handleHttpError = (statusCode, reject) => {
let errorMessage = '';
switch (statusCode) {
case 401:
errorMessage = '身份验证失败';
break;
case 403:
errorMessage = '您没有权限执行此操作';
break;
case 404:
errorMessage = '请求的资源不存在';
break;
case 500:
errorMessage = '服务器内部错误';
break;
default:
errorMessage = `网络错误: ${statusCode}`;
}
Toast(errorMessage);
reject(new Error(errorMessage));
};
/**
* 处理请求参数,格式化uni.request所需配置
* @param {Object} options - 原始请求配置
* @returns {Object} - 格式化后的请求参数
*/
const dealRequestParams = (options) => {
// 拼接请求URL(支持直接传url或通过api配置获取)
let url = options.url ? options.url : API_CONFIG[options.api];
// 获取本地缓存的Token
let token = getToken();
// 若请求头中自定义了token,优先使用自定义值
if (options.header?.token) {
token = options.header.token;
}
// 构建默认请求头
let header = {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
token
};
// 格式化请求方法(默认GET,转为大写)
options.method = options.method ? options.method.toUpperCase() : 'GET';
// 若请求数据包含数组,修改content-type为json格式
if (options.data) {
for (let i in options.data) {
if (Array.isArray(options.data[i])) {
header['content-type'] = 'application/json';
break;
}
}
}
// 返回格式化后的请求参数
return {
url: BASE_URL + url,
method: options.method,
data: options.data || {},
header: options.header || header
};
};
// 导出自定义请求方法
export { myHttp };
三、核心逻辑解析
3.1 全局变量作用(实现“仅刷新一次”核心)
refreshPromise:存储刷新 Token 的 Promise 实例,作为刷新锁。- 为
null时:表示当前无刷新操作,可发起新的刷新请求。 - 不为
null时:表示正在刷新 Token,后续过期请求直接加入队列,无需重复刷新。
- 为
requestQueue:存储 Token 过期期间所有失败的请求,等待新 Token 获取后统一重试。
3.2 核心流程梳理
- 正常请求:调用
myHttp(),若未在刷新 Token,直接构建请求参数并发起uni.request。 - Token 过期拦截:接口返回
code = -1,将当前请求加入队列,判断是否正在刷新 Token。 - 首次刷新 Token:
refreshPromise为null,创建新 Promise,调用wxLogin()获取 code,再调用静默登录接口获取新 Token。 - 刷新成功处理:缓存新 Token,遍历请求队列,替换 Token 后逐个重试请求,执行原 Promise 回调。
- 刷新失败处理:清空请求队列,逐个执行失败回调,给出用户友好提示。
- 释放刷新锁:刷新流程(成功/失败)结束后,将
refreshPromise置为null,允许后续可能的刷新操作。
3.3 关键方法说明
| 方法名 | 核心作用 | 注意点 |
|---|---|---|
myHttp() |
对外暴露的请求入口,实现请求队列缓存逻辑 | skipRefreshLock 为 true 时跳过队列缓存,用于刷新 Token 的内部请求 |
handleTokenExpiration() |
统一处理 Token 过期,控制刷新逻辑和队列缓存 | 确保 refreshPromise 仅赋值一次,避免并发刷新 |
retryQueuedRequests() |
重试所有缓存请求 | 先复制队列再清空,避免重试过程中新增请求干扰 |
clearRequestQueue() |
刷新失败时清空队列 | 遍历队列执行 reject,将错误信息透传给原请求 |
dealRequestParams() |
格式化请求参数 | 自动适配 form-urlencoded/json 格式,支持自定义 Token |
四、使用说明与注意事项
4.1 正常使用
// 引入封装的myHttp方法
import { myHttp } from './http.js';
//或者myHttp挂载到vue或者uni
uni.$http = myHttp;
Vue.prototype.$http = myHttp;
// 发起业务请求
myHttp({
api: 'userInfo', // 对应API_CONFIG中的配置
method: 'get',
data: {
userId: 123
}
}).then(res => {
// 处理接口返回数据
console.log('用户信息:', res);
}).catch(err => {
// 处理请求失败(包括Token刷新失败、网络错误等)
console.error('请求失败:', err);
});
更多推荐


所有评论(0)