在移动应用开发中,与后端 API 的通信是核心环节。原生的 uni.request 虽然功能完备,但在大型项目中直接使用会带来代码分散、难以维护、Token 管理复杂等问题。一套优秀的全局请求封装需要统一处理请求头、参数格式、错误反馈、Token 过期自动刷新以及重试机制。本文将手把手教你从零搭建 uni-app 的请求工具库,实现请求/响应拦截器和双 Token 无感刷新,让你的应用与后端沟通更稳健、更优雅。
为什么需要请求封装与 Token 刷新?
如果你在每个页面中都直接调用 uni.request,很快会遇到以下问题:
- 重复配置:所有请求都要手动设置 baseUrl、header。
- 错误处理分散:没有统一的报错提示和状态码转换。
- Token 过期处理复杂:用户可能在操作中突然遇到 401,必须重新登录。
- 并发刷新冲突:多个请求同时过期,重复请求刷新接口,造成浪费甚至死锁。
通过集中封装 request 并实现双 Token(accessToken + refreshToken)无感刷新,可以让用户在不知情的情况下自动续期,同时避免重复刷新,保障体验流畅。下面我们将逐步构建这样一个工具。
基础封装:创建 Request 类
首先在项目目录创建 utils/request.js,定义一个 Request 类,基础功能包括配置合并、请求发起、GET/POST 快捷方法。
// utils/request.js
class Request {
constructor() {
this.baseUrl = 'https://api.example.com'
this.header = {
'Content-Type': 'application/json'
}
}
// 合并配置
mergeConfig(customConfig) {
const config = {
url: this.baseUrl + customConfig.url,
method: (customConfig.method || 'GET').toUpperCase(),
header: { ...this.header, ...(customConfig.header || {}) },
data: customConfig.data || {},
timeout: customConfig.timeout || 15000
}
return config
}
// 核心请求方法
request(options) {
const config = this.mergeConfig(options)
return new Promise((resolve, reject) => {
uni.request({
...config,
success: (res) => {
// 后续可经过响应拦截器处理
resolve(res.data)
},
fail: (err) => {
reject(err)
}
})
})
}
get(url, params = {}, options = {}) {
return this.request({
url,
method: 'GET',
data: params,
...options
})
}
post(url, data = {}, options = {}) {
return this.request({
url,
method: 'POST',
data,
...options
})
}
}
export default new Request()
此时已经可以通过 request.get('/user/info') 等快捷方法发起请求,但还需要添加拦截机制和 Token 管理功能。
请求与响应拦截器设计
拦截器让我们在请求发出前和收到响应后插入自定义逻辑。我们可以在 Request 类中维护两个拦截器数组,并在请求流程中依次调用。
扩展 utils/request.js:
class Request {
constructor() {
this.baseUrl = 'https://api.example.com'
this.header = { 'Content-Type': 'application/json' }
this.interceptors = {
request: [], // 请求拦截器队列
response: [] // 响应拦截器队列
}
}
// 添加请求拦截器
addRequestInterceptor(fn) {
this.interceptors.request.push(fn)
}
// 添加响应拦截器
addResponseInterceptor(fn) {
this.interceptors.response.push(fn)
}
async runRequestInterceptors(config) {
let result = config
for (const interceptor of this.interceptors.request) {
result = interceptor(result)
}
return result
}
async runResponseInterceptors(responseData) {
let result = responseData
for (const interceptor of this.interceptors.response) {
result = interceptor(result)
}
return result
}
async request(options) {
let config = this.mergeConfig(options)
// 执行请求拦截器
config = await this.runRequestInterceptors(config)
return new Promise((resolve, reject) => {
uni.request({
...config,
success: async (res) => {
let processed = res.data
try {
// 执行响应拦截器
processed = await this.runResponseInterceptors(processed, res)
} catch (e) {
return reject(e)
}
resolve(processed)
},
fail: (err) => {
reject(err)
}
})
})
}
}
现在我们可以通过 request.addRequestInterceptor 和 addResponseInterceptor 注册全局处理函数。例如,在请求拦截器中自动附加 Token,在响应拦截器中统一处理错误码。
双 Token 机制与无感刷新原理
为了保证安全同时兼顾体验,后端通常提供两个 Token:access_token(短时效,如2小时)和 refresh_token(长时效,如30天)。access_token 用于业务请求,refresh_token 仅用于获取新的 access_token。
无感刷新的核心流程是:当业务请求收到 401(或自定义错误码)时,使用 refresh_token 调用刷新接口获取新的 access_token,然后重试原请求。为了防止多个请求同时触发刷新,需要引入刷新锁和请求队列:
let isRefreshing = false
let pendingRequests = [] // 等待刷新的请求队列
// 存储 token 的键名
const ACCESS_TOKEN_KEY = 'access_token'
const REFRESH_TOKEN_KEY = 'refresh_token'
当第一个请求发现 token 过期时,设置 isRefreshing = true,并将后续请求缓存到队列中。刷新成功后,批量用新 token 重试队列里的请求,最后重置标志位。
案例实战:自动刷新与排队重试
在请求封装中整合 Token 管理逻辑。我们修改 request 方法,在成功回调里根据返回的状态码判断是否需要刷新;同时利用队列处理并发情况。
完整的核心代码:
// 存储与读取 Token(这里简化,实际可用 uni.getStorageSync)
function getAccessToken() {
return uni.getStorageSync(ACCESS_TOKEN_KEY) || ''
}
function getRefreshToken() {
return uni.getStorageSync(REFRESH_TOKEN_KEY) || ''
}
function setTokens(access, refresh) {
uni.setStorageSync(ACCESS_TOKEN_KEY, access)
if (refresh) uni.setStorageSync(REFRESH_TOKEN_KEY, refresh)
}
// 刷新 Token 的方法
async function refreshAccessToken() {
const refreshToken = getRefreshToken()
if (!refreshToken) {
// 无 refresh_token,强制登出
uni.reLaunch({ url: '/pages/login/index' })
return Promise.reject('登录已过期')
}
try {
const res = await uni.request({
url: 'https://api.example.com/auth/refresh',
method: 'POST',
data: { refresh_token: refreshToken }
})
const data = res.data
if (data.code === 200) {
const newAccess = data.data.access_token
setTokens(newAccess, data.data.refresh_token || refreshToken)
return newAccess
} else {
// 刷新失败,清除 token 并跳转登录
uni.removeStorageSync(ACCESS_TOKEN_KEY)
uni.removeStorageSync(REFRESH_TOKEN_KEY)
uni.reLaunch({ url: '/pages/login/index' })
return Promise.reject('登录已过期')
}
} catch (err) {
return Promise.reject('刷新请求异常')
}
}
// 在 Request 类的请求方法中嵌入刷新逻辑
class Request {
// ... 前面的代码
async request(options) {
let config = this.mergeConfig(options)
config = await this.runRequestInterceptors(config)
return new Promise((resolve, reject) => {
const originalResolve = resolve
const originalReject = reject
uni.request({
...config,
success: async (res) => {
let data = res.data
// 如果返回未授权且存在 refresh_token,尝试刷新
if (data.code === 401 && getRefreshToken()) {
if (!isRefreshing) {
isRefreshing = true
try {
const newToken = await refreshAccessToken()
// 刷新成功,更新请求头并重试原请求
config.header.Authorization = 'Bearer ' + newToken
uni.request({
...config,
success: (retryRes) => {
resolve(retryRes.data)
},
fail: (retryErr) => {
reject(retryErr)
}
})
// 刷新成功后,重试队列中的其它请求
pendingRequests.forEach(cb => cb(newToken))
pendingRequests = []
} catch (refreshErr) {
// 刷新失败,拒绝所有等待的请求
originalReject(refreshErr)
pendingRequests.forEach(cb => cb.reject(refreshErr))
pendingRequests = []
} finally {
isRefreshing = false
}
} else {
// 已有刷新进行中,将当前请求放入队列等待
pendingRequests.push({
resolve: originalResolve,
reject: originalReject,
config: config
})
}
} else {
// 正常业务码或无需刷新的401
try {
data = await this.runResponseInterceptors(data, res)
resolve(data)
} catch (e) {
reject(e)
}
}
},
fail: (err) => {
reject(err)
}
})
})
}
}
队列中的请求在刷新成功后,需要手动使用新 token 重新发起。上面的代码在刷新成功后,遍历 pendingRequests,对于每个缓存的请求,可以取出其 config 并用新 token 重试;为了简化,上面示例仅重试了第一个触发刷新的请求,实际应该对队列中每个请求都执行类似的重试。更完善的实现是将每个挂起请求的 resolve/reject 保存,并在获得新 token 后为它们重新执行 request 并连接对应的 resolve/reject。
补充队列重试细节:
// 在刷新成功后,处理队列
pendingRequests.forEach(async (item) => {
const newConfig = { ...item.config, header: { ...item.config.header, Authorization: 'Bearer ' + newToken } }
try {
const retryRes = await uni.request(newConfig)
item.resolve(retryRes.data)
} catch (err) {
item.reject(err)
}
})
至此,自动刷新机制搭建完毕。用户在操作过程中完全感觉不到 Token 过期,即便多个接口同时过期,也只会在后台执行一次刷新。
API 模块化管理与使用示例
为了避免将接口地址散落在页面中,建议按业务模块建立 API 文件。例如 api/user.js:
// api/user.js
import request from '@/utils/request'
export function getUserInfo() {
return request.get('/user/info')
}
export function updateProfile(data) {
return request.post('/user/profile', data)
}
在页面中调用:
import { getUserInfo } from '@/api/user'
uni.showLoading({ title: '加载中' })
try {
const user = await getUserInfo()
console.log(user)
} catch (e) {
uni.showToast({ title: '请求失败', icon: 'none' })
} finally {
uni.hideLoading()
}
你还可以在请求拦截器中统一添加 loading 和 全局错误提示,减少业务代码中的重复逻辑。
模拟测试与最佳实践
使用 Postman 或 Mock 服务可以模拟 access_token 过期场景。首先正常登录获取 token,然后在后端将 access_token 有效期设短(或手动删除存储中的 access_token),接着发起多个并发请求,观察控制台是否只调用一次刷新接口,其余请求在刷新成功后自动重试并正确返回数据。
在生产中,还需要注意以下几点:
- 刷新接口本身不应被拦截刷新:否则会死循环。请求刷新接口时不要携带 access_token 或单独判断。
- 存储安全:在小程序中,
uni.setStorageSync可能被逆向,敏感场景应使用加密存储或服务端限制。 - 并发极限:如果短时间内大量请求同时触发 401,队列可能积压。可以设置最大重试次数,或对刷新接口追加限流。
- 回退处理:当 refresh_token 也过期时,必须清除本地状态并引导用户重新登录,不要让用户陷入无限刷新循环。
- 平台兼容:uni-app 的
uni.request在不同端表现一致,上述封装在 H5、小程序、App 中均可运行。
总结
通过本文的实战,我们构建了一套功能完善的 uni-app 网络请求封装,包括统一配置、请求/响应拦截器、双 Token 无感刷新以及并发刷新队列管理。这套工具库能够大幅降低与后端交互的复杂度,提升用户体验和代码的可维护性。现在你可以将它集成到自己的 uni-app 项目中,并根据实际业务需求进行扩展,比如加入签名算法、多环境切换或缓存策略。

