uni-app 全局请求封装与 Token 无感刷新实战:构建健壮的 API 交互层

2026-06-09 0 194

在移动应用开发中,与后端 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.addRequestInterceptoraddResponseInterceptor 注册全局处理函数。例如,在请求拦截器中自动附加 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 项目中,并根据实际业务需求进行扩展,比如加入签名算法、多环境切换或缓存策略。

uni-app 全局请求封装与 Token 无感刷新实战:构建健壮的 API 交互层
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app 全局请求封装与 Token 无感刷新实战:构建健壮的 API 交互层 https://www.taomawang.com/web/uniapp/2116.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务