UniApp 请求封装实战:优雅实现 Token 无感刷新(附完整代码)

在 UniApp 跨端开发中,基于 Token 的接口鉴权是最常见的身份验证方式。但 Token 有过期时间,传统处理方式是遇到 401 状态码直接跳转登录页,用户体验极差 ------ 比如用户正在填写表单,突然提示登录过期,之前的操作全部白费。

本文将分享一套生产级的 UniApp request 请求封装方案,核心实现Token 过期后的无感刷新:无需用户手动操作,自动刷新 Token 并重试所有过期请求,仅当刷新 Token 失败时才引导用户重新登录。

核心思路

无感刷新的关键是解决「多个请求同时触发 401」和「刷新 Token 期间的请求等待」问题,核心逻辑如下:

  1. 拦截接口返回的 401 状态码(Token 过期);
  2. isRefreshing标记位防止多个请求同时调用刷新 Token 接口;
  3. 将所有触发 401 的请求存入requests队列,等待 Token 刷新完成后统一重试;
  4. 调用刷新 Token 接口获取新 Token,更新本地存储;
  5. 用新 Token 重试队列中的所有请求,实现「无感」体验;
  6. 若刷新 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函数是整个无感刷新的核心:

  1. 重试封装retryRequest函数负责用新 Token 重新发起原请求;
  2. 队列等待:若正在刷新 Token,将重试函数加入队列;
  3. 刷新 Token :未刷新时标记状态,调用refreshToken接口;
  4. 刷新成功:更新 Token、重试队列所有请求、重试当前请求;
  5. 刷新失败:弹出确认框,引导用户登录(携带原页面路径,登录后可返回);
  6. 状态重置 :无论成败,最终解除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.requestgetCurrentPages是 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。

注意事项

  1. 后端约定:必须和后端约定好 Token 规则(过期状态码 401、刷新接口地址、RefreshToken 传递方式);
  2. RefreshToken 存储 :建议RefreshToken单独存储,且有效期设置为 7 天 / 30 天(长于 Access Token 的 2 小时);
  3. 并发请求:该方案完美处理并发请求的 401 问题,比如页面初始化时多个接口同时返回 401,只会触发一次刷新;
  4. 登出逻辑:若项目有 Vuex 的登出 action,可替换代码中直接跳转登录的逻辑,清空用户信息。
flowchart TD Start[开始请求] --> CheckToken{是否需要Token?} CheckToken -->|是| AddToken[添加Authorization头] CheckToken -->|否| ProcessURL AddToken --> ProcessURL[处理URL参数] ProcessURL --> Request[发送uni.request请求] Request --> Response{请求结果} Response -->|网络异常| NetworkError[显示错误并拒绝] Response -->|成功返回| CheckCode{检查响应码} NetworkError --> End[结束] CheckCode -->|200| Success[返回数据并解析] CheckCode -->|500| ServerError[显示错误并拒绝] CheckCode -->|401| Handle401["处理401错误
(核心逻辑)
"] 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 等多端,可直接复制使用,也可根据业务需求扩展(如添加请求防抖、缓存等)。

相关推荐
2501_915918412 小时前
使用 HBuilder 上架 iOS 应用时常见的问题与应对方式
android·ios·小程序·https·uni-app·iphone·webview
2501_916007474 小时前
iOS 崩溃日志的分析方法,将崩溃日志与运行过程结合分析
android·ios·小程序·https·uni-app·iphone·webview
2501_916007474 小时前
React Native 混淆在真项目中的方式,当 JS 和原生同时暴露
javascript·react native·react.js·ios·小程序·uni-app·iphone
00后程序员张5 小时前
苹果应用商店上架App流程,签名证书、IPA 校验、上传
android·ios·小程序·https·uni-app·iphone·webview
2501_916007475 小时前
iOS 上架需要哪些准备,围绕证书、描述文件和上传方式等关键环节展开分析
android·ios·小程序·https·uni-app·iphone·webview
2501_915106325 小时前
iOS 上架费用解析,哪些成本可以通过流程优化降低。
android·ios·小程序·https·uni-app·iphone·webview
小离a_a7 小时前
uniapp微信小程序实现拍照加水印,水印上添加当前时间,当前地点等信息,地点逆解析使用的是高德地图
微信小程序·小程序·uni-app
前端小雪的博客.8 小时前
uniapp小程序顶部状态栏占位和自定义头部导航栏
小程序·uni-app
2501_916007471 天前
iOS 证书如何创建,从能生成到能长期使用
android·macos·ios·小程序·uni-app·cocoa·iphone