基于 Promise 队列的前端鉴权无感续签:实现跨页面请求拦截与自动重发
问题背景:Token 过期处理的常见痛点
在现代前端应用中,用户身份认证普遍依赖 Token。然而,当 Token 失效时,许多项目仍采用简单粗暴的方式处理——一旦接口返回 401 状态码,立即跳转登录页或清空状态。这种做法在多请求并发场景下极易引发以下问题:
- 用户体验断裂:用户正在操作表单或浏览内容,突然被强制跳转,导致输入数据丢失。
- 界面混乱:多个请求几乎同时触发 401,可能弹出多个提示框,甚至重复打开登录页面。
本文将介绍一种利用 JavaScript 闭包与 Promise 队列机制构建的优雅解决方案,实现在不中断用户操作的前提下完成重新登录并恢复所有待处理请求。
设计模式:请求协调器(Request Coordinator)
我们可以将整个流程类比为机场安检通道:
- 多个乘客(HTTP 请求)排队过检,发现证件无效(Token 过期)。
- 安检员暂停放行,但不驱散人群,而是引导至等候区暂存(进入 Promise 队列)。
- 系统仅启动一次身份验证流程(如弹出全局登录模态窗),避免重复触发。
- 用户完成认证后,安检通道重新开放,所有积压请求依次使用新凭证重试。
该模式的核心在于统一协调、串行处理、批量恢复,确保逻辑集中且不影响上层业务代码结构。
核心实现:可复用的请求管理模块
以下是一个独立封装的请求处理器,适用于 UniApp 或类似运行环境,完全剥离 UI 层依赖。
import store from '@/store';
import config from '@/config';
// 🔒 全局锁:防止多次弹窗
let isAuthDialogActive = false;
// 📥 挂起队列:存储待恢复的请求上下文
let pendingQueue = [];
/**
* 外部调用入口:用于通知认证结果,驱动队列执行
* @param {boolean} authenticated 是否认证成功
*/
export const resumePendingRequests = (authenticated) => {
isAuthDialogActive = false;
pendingQueue.forEach(context => {
if (authenticated) {
context.retry(); // 使用最新 Token 重发
} else {
context.reject(new Error('AUTH_CANCELLED'));
}
});
pendingQueue = [];
};
/**
* 统一请求方法
* @param {Object} options 请求配置
* @returns {Promise}
*/
export default function fetchApi(options) {
const {
url = '',
method = 'GET',
data = {},
header = {},
custom = {}
} = options;
const { needAuth = false, showLoading = false, hideError = false } = custom;
return new Promise((resolve, reject) => {
// 将请求逻辑包裹成可重复执行的函数(形成闭包)
const retryRequest = () => {
const requestHeader = {
'Content-Type': 'application/json',
...header
};
// 若需鉴权,注入当前 Token
if (needAuth) {
const token = uni.getStorageSync('token');
if (!token) {
interceptAuth(retryRequest, reject, showLoading);
return;
}
requestHeader['X-Access-Token'] = token;
}
if (showLoading) uni.showLoading({ title: '加载中...' });
uni.request({
url: config.baseURL + url,
method,
data,
header: requestHeader,
success: (res) => {
if (showLoading) uni.hideLoading();
if (res.statusCode === 200 && res.data?.code === 200) {
resolve(res.data);
return;
}
// 业务级错误处理
const msg = res.data?.message || '请求异常';
if (!hideError) {
uni.showToast({ title: msg, icon: 'none' });
}
reject({ code: res.data?.code, message: msg });
// HTTP 状态码拦截
} else {
const errorInfo = { code: res.statusCode, message: '网络错误' };
switch (res.statusCode) {
case 401:
case 403:
interceptAuth(retryRequest, reject, showLoading);
return;
case 404:
errorInfo.message = '接口不存在'; break;
case 500:
errorInfo.message = '服务器内部错误'; break;
}
if (!hideError) {
uni.showToast({ title: errorInfo.message, icon: 'none' });
}
reject(errorInfo);
}
},
fail: (err) => {
if (showLoading) uni.hideLoading();
if (!hideError) uni.showToast({ title: '网络连接失败', icon: 'none' });
reject({ code: -1, message: err.errMsg });
}
});
});
// 初始执行
retryRequest();
}
/**
* 401 拦截处理逻辑
* @param {Function} retryFn 重试函数
* @param {Function} rejectFn 拒绝函数
* @param {boolean} loading 当前是否有 loading
*/
function interceptAuth(retryFn, rejectFn, loading) {
if (loading) uni.hideLoading();
// 加入挂起队列
pendingQueue.push({ retry: retryFn, reject: rejectFn });
// 只触发一次认证弹窗
if (!isAuthDialogActive) {
isAuthDialogActive = true;
uni.removeStorageSync('token'); // 清除旧凭证
store.commit('login/SHOW_LOGIN_MODAL', true);
uni.showToast({ title: '请重新登录', icon: 'none' });
}
}
组件集成:如何响应认证结果
在你的全局登录组件中,只需调用导出的方法即可唤醒所有等待中的请求:
<script setup>
import { resumePendingRequests } from '@/utils/fetchApi';
const onSubmit = async () => {
// 执行登录逻辑...
const result = await loginAPI(credentials);
if (result.code === 200) {
uni.setStorageSync('token', result.data.token);
// ✅ 通知请求管理器继续工作
resumePendingRequests(true);
store.commit('login/SHOW_LOGIN_MODAL', false);
}
};
const onCancel = () => {
// ❌ 用户取消,终止所有挂起请求
resumePendingRequests(false);
store.commit('login/SHOW_LOGIN_MODAL', false);
};
</script>
优势总结
此架构具备以下显著优点:
- 业务解耦:页面无需感知 Token 刷新过程,await 调用自然延续。
- 线程安全:通过全局锁控制并发,杜绝重复弹窗和资源竞争。
- 状态一致:所有请求共享同一轮认证周期,保证数据一致性。
- 易于维护:逻辑集中在底层模块,升级策略不影响业务组件。
通过合理运用 JavaScript 的异步特性和状态管理,我们能够构建出既健壮又透明的身份验证体系,真正实现"无感刷新"的用户体验目标。