在 UniApp 跨端开发中,基于 Token 的接口鉴权是最常见的身份验证方式。但 Token 有过期时间,传统处理方式是遇到 401 状态码直接跳转登录页,用户体验极差 ------ 比如用户正在填写表单,突然提示登录过期,之前的操作全部白费。
本文将分享一套生产级的 UniApp request 请求封装方案,核心实现Token 过期后的无感刷新:无需用户手动操作,自动刷新 Token 并重试所有过期请求,仅当刷新 Token 失败时才引导用户重新登录。
核心思路
无感刷新的关键是解决「多个请求同时触发 401」和「刷新 Token 期间的请求等待」问题,核心逻辑如下:
- 拦截接口返回的 401 状态码(Token 过期);
- 用
isRefreshing标记位防止多个请求同时调用刷新 Token 接口; - 将所有触发 401 的请求存入
requests队列,等待 Token 刷新完成后统一重试; - 调用刷新 Token 接口获取新 Token,更新本地存储;
- 用新 Token 重试队列中的所有请求,实现「无感」体验;
- 若刷新 Token 失败(如 RefreshToken 过期),则引导用户重新登录。
完整代码实现
以下是封装后的request.js完整代码,包含详细注释,可直接接入项目使用:
javascript
运行
javascript
import store from '@/store'
import config from '@/config'
// 封装Token的存储/获取/清除(建议单独抽离,方便替换存储方式)
import {
getToken,
setToken,
clearToken,
setRefreshToken,
getRefreshToken,
removeRefreshToken
} from '@/utils/auth'
import errorCode from '@/utils/errorCode' // 错误码映射表
import { toast, showConfirm, tansParams } from '@/utils/common' // 通用工具函数
// 请求超时时间
let timeout = 10000
// 接口基础地址(从配置文件读取)
const baseUrl = config.baseUrl
// 核心标记:是否正在刷新Token(避免重复刷新)
let isRefreshing = false
// 核心队列:存储401失败的请求,刷新Token后重试
let requests = []
/**
* 封装后的请求核心函数
* @param {Object} config - 请求配置(url/method/data/header等)
* @returns {Promise}
*/
const request = config => {
// 是否需要携带Token(部分接口无需鉴权,可通过header.isToken=false关闭)
const isToken = (config.headers || {}).isToken === false
config.header = config.header || {}
// 携带Token(Bearer认证格式)
if (getToken() && !isToken) {
config.header['Authorization'] = 'Bearer ' + getToken()
}
// GET请求参数拼接(将params转为url参数)
if (config.params) {
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.url = url
}
return new Promise((resolve, reject) => {
uni.request({
method: config.method || 'GET',
timeout: config.timeout || timeout,
url: baseUrl + config.url.replace('/api', ''), // 适配后端接口路径(按需调整)
data: config.data,
header: config.header,
dataType: 'json'
}).then(response => {
let [error, res] = response
// 网络层错误(如超时、断网)
if (error) {
toast('系统异常请联系管理员')
reject('系统异常请联系管理员')
return
}
// 业务层状态码处理
const code = res.data.code || 200
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 核心:拦截401 Token过期
if (code === 401) {
handle401Error(config, resolve, reject)
}
// 500服务器错误
else if (code === 500) {
toast(msg)
reject('500')
}
// 其他业务错误
else if (code !== 200) {
toast(msg)
reject(code)
}
// 请求成功
else {
resolve(res.data)
}
}).catch(error => {
// 网络异常兜底处理
let { message } = error
if (message === 'Network Error') {
message = '后端接口连接异常'
} else if (message.includes('timeout')) {
message = '系统接口请求超时'
} else if (message.includes('Request failed with status code')) {
message = '系统接口' + message.substr(message.length - 3) + '异常'
}
toast(message)
reject(error)
})
})
}
/**
* 核心:401错误处理(无感刷新Token + 重试请求)
* @param {Object} config - 原请求配置
* @param {Function} resolve - 原请求的resolve
* @param {Function} reject - 原请求的reject
*/
function handle401Error(config, resolve, reject) {
// 封装重试逻辑:用新Token重新发起请求
const retryRequest = () => {
if (getToken()) {
config.header['Authorization'] = 'Bearer ' + getToken()
}
request(config).then(res => resolve(res)).catch(err => reject(err))
}
// 若正在刷新Token,将当前请求加入队列等待
if (isRefreshing) {
requests.push(retryRequest)
return
}
// 标记为「正在刷新Token」,防止并发请求重复刷新
isRefreshing = true
// 调用刷新Token接口
refreshToken()
.then((newToken) => {
// 刷新成功:更新Token + 重试所有排队请求
setToken(newToken) // 存储新Token
requests.forEach(cb => cb()) // 批量重试队列中的请求
requests = [] // 清空队列
retryRequest() // 重试当前请求
})
.catch(() => {
// 刷新失败(如RefreshToken过期):引导用户重新登录
showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(res => {
if (res.confirm) {
// 优化:记录当前页面路径,登录后可跳转回来
const currentPages = getCurrentPages()
const currentPage = currentPages[currentPages.length - 1]
const redirectUrl = currentPage ? currentPage.route : 'pages/index/index'
// 跳转登录页(替换为你的实际登录页路径)
uni.reLaunch({
url: `/pages/login/login?redirect=${redirectUrl}`
})
}
})
// 拒绝所有排队请求
requests.forEach(cb => cb())
requests = []
reject('会话已过期,请重新登录')
})
.finally(() => {
// 无论刷新成功/失败,解除「正在刷新」标记
isRefreshing = false
})
}
/**
* 调用后端刷新Token接口
* @returns {Promise<string>} 新的Access Token
*/
function refreshToken() {
return new Promise((resolve, reject) => {
uni.request({
url: baseUrl + '/system/appUser/app/refreshToken', // 替换为你的刷新Token接口
method: 'POST',
header: {
// 携带RefreshToken(后端需约定鉴权方式)
'refreshToken': getRefreshToken()
},
timeout: timeout,
success: (res) => {
const { code, token } = res.data
// 刷新成功(需后端返回新Token)
if (code === 200 && token) {
resolve(token)
} else {
reject(new Error('刷新Token失败'))
}
},
fail: (err) => {
toast('刷新登录状态失败')
reject(err)
}
})
})
}
export default request
关键模块解析
1. 辅助层准备(必看)
封装前需要先实现 3 个辅助文件,保证代码可运行:
-
auth.js:Token 的本地存储(基于uni.setStorageSync)javascript
运行
javascript// 存储Token export const setToken = (token) => uni.setStorageSync('token', token) // 获取Token export const getToken = () => uni.getStorageSync('token') || '' // 清除Token export const clearToken = () => uni.removeStorageSync('token') // RefreshToken同理(略) -
errorCode.js:业务错误码映射(如 401="登录过期") -
common.js:通用工具函数(toast 提示、confirm 确认框、参数拼接)
2. 核心状态控制
isRefreshing:布尔值,标记是否正在刷新 Token,防止多个 401 请求同时调用刷新接口,造成资源浪费;requests:数组,存储所有因 401 失败的请求重试函数,刷新完成后批量执行。
3. 401 处理逻辑(核心)
handle401Error函数是整个无感刷新的核心:
- 重试封装 :
retryRequest函数负责用新 Token 重新发起原请求; - 队列等待:若正在刷新 Token,将重试函数加入队列;
- 刷新 Token :未刷新时标记状态,调用
refreshToken接口; - 刷新成功:更新 Token、重试队列所有请求、重试当前请求;
- 刷新失败:弹出确认框,引导用户登录(携带原页面路径,登录后可返回);
- 状态重置 :无论成败,最终解除
isRefreshing标记。
4. 刷新 Token 接口
refreshToken函数调用后端的刷新接口,注意:
- 后端需提供专门的刷新 Token 接口(如
/app/refreshToken); - 请求头需携带
RefreshToken(有效期长于 Access Token); - 成功后返回新的 Access Token,失败则触发登录逻辑。
使用示例
封装完成后,在 API 层直接调用即可,无需关心 Token 刷新逻辑:
javascript
运行
javascript
// api/user.js
import request from '@/utils/request'
// 获取用户信息
export const getUserInfo = () => {
return request({
url: '/system/user/info',
method: 'GET'
})
}
// 提交表单
export const submitForm = (data) => {
return request({
url: '/system/form/submit',
method: 'POST',
data
})
}
实战优化点
1. 登录跳转优化
代码中已实现「跳转登录页携带原页面路径」,登录成功后可解析redirect参数,跳转回用户之前操作的页面:
javascript
运行
javascript
// 登录页逻辑
onLoad(options) {
this.redirectUrl = options.redirect || 'pages/index/index'
},
// 登录成功后
uni.reLaunch({
url: `/${this.redirectUrl}`
})
2. 边界场景处理
-
多端兼容 :
uni.request和getCurrentPages是 UniApp 跨端 API,H5 / 小程序 / APP 均兼容; -
网络异常:对超时、断网等情况做了兜底提示,提升用户感知;
-
无 Token 请求 :部分接口(如登录、注册)无需鉴权,可通过
header.isToken=false关闭 Token 携带:javascript
运行
php// 登录接口(无需Token) export const login = (data) => { return request({ url: '/system/login', method: 'POST', data, header: { isToken: false } }) }
3. 生产环境适配
- 可将
baseUrl按环境区分(开发 / 测试 / 生产); - 增加请求 / 响应拦截器(如添加日志、加密参数);
- 结合 Vuex 管理用户状态,刷新 Token 后同步更新 store。
注意事项
- 后端约定:必须和后端约定好 Token 规则(过期状态码 401、刷新接口地址、RefreshToken 传递方式);
- RefreshToken 存储 :建议
RefreshToken单独存储,且有效期设置为 7 天 / 30 天(长于 Access Token 的 2 小时); - 并发请求:该方案完美处理并发请求的 401 问题,比如页面初始化时多个接口同时返回 401,只会触发一次刷新;
- 登出逻辑:若项目有 Vuex 的登出 action,可替换代码中直接跳转登录的逻辑,清空用户信息。
(核心逻辑)"] CheckCode -->|其他非200| OtherError[显示错误并拒绝] Success --> End ServerError --> End OtherError --> End Handle401 --> CheckRefreshing{"是否正在刷新Token?
(防重复机制)"} CheckRefreshing -->|是| AddToQueue["加入请求队列等待"] CheckRefreshing -->|否| SetRefreshing["设置刷新标记为true"] AddToQueue --> Wait[等待刷新完成] Wait --> End SetRefreshing --> CallRefresh["调用refreshToken接口
/system/appUser/app/refreshToken"] CallRefresh --> RefreshResult{"刷新结果
(成功/失败)"} RefreshResult -->|成功| UpdateToken["更新Token
清空RefreshToken"] RefreshResult -->|失败| ShowConfirm["显示登录过期确认框
(用户选择是否重新登录)"] UpdateToken --> RetryQueue["重试队列中所有请求
(requests.forEach)"] RetryQueue --> RetryCurrent["重试当前请求
(核心重试逻辑)"] RetryCurrent --> ClearQueue[清空请求队列] ClearQueue --> ResetFlag[重置刷新标记为false] ResetFlag --> End ShowConfirm --> ConfirmResult{用户选择} ConfirmResult -->|确认| Logout["跳转到登录页
(带redirect参数)"] ConfirmResult -->|取消| Continue Logout --> RejectQueue["拒绝队列中所有请求"] Continue --> RejectQueue RejectQueue --> RejectCurrent["拒绝当前请求"] RejectCurrent --> ResetFlag2[重置刷新标记为false] ResetFlag2 --> End %% 样式定义 classDef highlightNode fill:#fff2cc,stroke:#d6b656,stroke-width:3px,font-weight:bold classDef processNode fill:#d5e8d4,stroke:#82b366,stroke-width:2px classDef decisionNode fill:#ffe6cc,stroke:#d79b00,stroke-width:2px classDef errorNode fill:#f8cecc,stroke:#b85450,stroke-width:2px classDef endNode fill:#e1d5e7,stroke:#9673a6,stroke-width:2px %% 应用样式 class Handle401,CallRefresh,UpdateToken,RetryQueue,RetryCurrent,Logout highlightNode class CheckRefreshing,RefreshResult,ConfirmResult decisionNode class NetworkError,ServerError,OtherError,RejectQueue,RejectCurrent errorNode class Start,End,Wait,ClearQueue,ResetFlag,ResetFlag2 endNode class AddToQueue,SetRefreshing,ShowConfirm processNode
总结
这套请求封装方案解决了 UniApp 中 Token 过期的核心痛点,实现了「无感刷新」的极致用户体验:
- 无需用户手动操作,Token 过期自动刷新;
- 并发请求不重复刷新,资源更高效;
- 刷新失败后友好引导登录,兼顾体验与安全。
该方案已在多个生产级 UniApp 项目中验证,适配小程序、H5、APP 等多端,可直接复制使用,也可根据业务需求扩展(如添加请求防抖、缓存等)。