更新记录

1.0.2(2026-07-19)

修复

  • 修正 TokenRefreshManager.getOrCreate 方法注释,补全了缺失的 @param globalConfig 参数说明,明确其用途为传递全局配置(如 apiFieldMap 字段映射),使 API 文档与实现保持一致。

优化

  • 增强代码可读性,完善内部 JSDoc 注释。

1.0.1(2026-07-19)

优化

  • token 刷新条件 code === 409 现支持动态字段名
  • 根据 globalConfig.apiFieldMap.code 自动适配后端返回的字段名(默认 'code')
  • 适配不同后端字段命名差异,如 code、status、errcode 等

1.0.0(2026-07-18)

新增

  • Token 无感刷新能力
  • 并发请求排队机制:刷新过程中后续请求自动排队,不重复刷新
  • 刷新成功后自动重试所有等待的请求
  • 自定义刷新策略:支持通过 shouldRefresh 自定义触发条件
  • 自定义请求头:支持通过 headerKeyheaderValue 自定义 Token 注入方式
  • 多实例灵活共享:支持通过 shareKey 控制多个 Request 实例共享独立刷新,按需切换
  • 支持普通请求(request)和文件上传(upload)两种模式
查看更多

平台兼容性

uni-app(3.8.0)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟

uni-app x(3.8.0)

Chrome Safari Android iOS 鸿蒙 微信小程序

mh-token-refresh

Token 无感刷新插件,Token 过期时自动刷新、并发请求排队不重复刷新、刷新后静默重试所有请求,全程用户无感知。

本插件是 mh-request 的配套插件,需配合使用。

特性

  • 🔄 无感刷新:Token 过期自动刷新,用户无感知
  • 🚦 并发排队:多个请求同时触发刷新时,仅执行一次,其余排队等待刷新完成后自动重试
  • 🔁 静默重试:刷新成功后自动重试所有等待的请求
  • 🔗 灵活共享:多 Request 实例可共享独立刷新,按需切换
  • 🎯 灵活配置:自定义刷新策略、请求头、Token 存储方式
  • 🔌 即插即用:作为 mh-request 插件,一行代码集成

快速开始

import { Request } from '@/uni_modules/mh-request'
import { tokenPlugin } from '@/uni_modules/mh-token-refresh'

const request = new Request({
  baseURL: 'https://api.example.com',
  silentCodes: [409],
  plugins: [
    {
      plugin: tokenPlugin,
      config: {
        getToken: () => uni.getStorageSync('token'),
        setToken: (token) => uni.setStorageSync('token', token),
        refreshToken: async () => {
          const refreshToken = uni.getStorageSync('refreshToken')
          const res = await uni.request({
            url: '/auth/refresh',
            method: 'POST',
            data: { refreshToken }
          })
          return { token: res.data.token }
        }
      }
    }
  ]
})

配置项

属性 类型 必填 默认值 说明
getToken () => string | null - 获取当前 Token
setToken (token: string) => void - 设置新 Token
refreshToken () => Promise<{ token: string }> - 刷新 Token 的异步方法
headerKey string 'ba-user-token' 请求头中的 Token Key
headerValue (token: string) => string (token) => token 请求头 Value 格式化函数
shouldRefresh (data: Record<string, any> | null) => boolean (data) => data?.code === 409 自定义刷新策略
shareKey string - 共享标识,相同标识的 Request 实例共用同一个 TokenRefreshManager

工作原理

并发排队机制

当多个请求同时返回 409(Token 过期)时:

请求A ──┐
请求B ──┼── 同时触发刷新 ──→ 仅执行 1 次刷新 ──→ 刷新成功 ──→ 重试 A、B、C
请求C ──┘

核心流程:

  1. 请求返回响应后,根据 shouldRefresh 判断是否需要刷新 Token
  2. 如果需要刷新且当前没有刷新任务,触发刷新
  3. 如果正在刷新中,当前请求进入等待队列,等待刷新完成后自动重试,不重复触发刷新
  4. 刷新成功后,更新 Token 并重试当前请求及队列中所有请求
  5. 刷新失败则清空队列并重置 Token

多实例模式

配置 行为 适用场景
不传 shareKey 每次新建 TokenRefreshManager 不同模块使用不同的 Token(如主应用 + 第三方服务)
传入相同 shareKey 复用同一个 TokenRefreshManager 同一套登录体系下的多个 Request 实例(如用户模块 + 订单模块)

高级用法

多实例共享刷新

import { Request } from '@/uni_modules/mh-request'
import { tokenPlugin } from '@/uni_modules/mh-token-refresh'

// 用户模块请求
const userRequest = new Request({
  baseURL: 'https://api.example.com/user',
  plugins: [{
    plugin: tokenPlugin,
    config: {
      shareKey: 'main',  // 共享标识
      getToken: () => uni.getStorageSync('token'),
      setToken: (token) => uni.setStorageSync('token', token),
      refreshToken: async () => {
        const res = await uni.request({ url: '/auth/refresh' })
        return { token: res.data.token }
      }
    }
  }]
})

// 订单模块请求
const orderRequest = new Request({
  baseURL: 'https://api.example.com/order',
  plugins: [{
    plugin: tokenPlugin,
    config: {
      shareKey: 'main',  // 相同标识,复用 TokenRefreshManager
      getToken: () => uni.getStorageSync('token'),
      setToken: (token) => uni.setStorageSync('token', token),
      refreshToken: async () => {
        const res = await uni.request({ url: '/auth/refresh' })
        return { token: res.data.token }
      }
    }
  }]
})

// userRequest 触发刷新时,orderRequest 的请求自动排队等待
// 刷新完成后,两者一起重试

多套 Token 独立刷新(不同标识)

// 主应用 Token
const mainRequest = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      shareKey: 'main',
      getToken: () => uni.getStorageSync('token'),
      setToken: (token) => uni.setStorageSync('token', token),
      refreshToken: async () => { /* 刷新主应用 token */ }
    }
  }]
})

// 第三方服务 Token(独立刷新)
const thirdPartyRequest = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      shareKey: 'third_party',  // 不同标识,独立刷新
      getToken: () => uni.getStorageSync('third_party_token'),
      setToken: (token) => uni.setStorageSync('third_party_token', token),
      refreshToken: async () => { /* 刷新第三方 token */ }
    }
  }]
})

独立实例(不传 shareKey)

// 不传 shareKey,每个 Request 实例拥有独立的 TokenRefreshManager
const requestA = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      // 没有 shareKey → 新建独立实例
      getToken: () => uni.getStorageSync('token_a'),
      setToken: (token) => uni.setStorageSync('token_a', token),
      refreshToken: async () => { /* 刷新 A */ }
    }
  }]
})

const requestB = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      // 没有 shareKey → 新建独立实例
      getToken: () => uni.getStorageSync('token_b'),
      setToken: (token) => uni.setStorageSync('token_b', token),
      refreshToken: async () => { /* 刷新 B */ }
    }
  }]
})
// requestA 和 requestB 各自维护刷新状态,互不影响

自定义刷新策略

const request = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      getToken: () => uni.getStorageSync('token'),
      setToken: (token) => uni.setStorageSync('token', token),
      refreshToken: async () => {
        const res = await uni.request({ url: '/auth/refresh' })
        return { token: res.data.token }
      },
      shouldRefresh: (data) => {
        return [401, 403, 409].includes(data?.code)
      }
    }
  }]
})

使用 Bearer Token

const request = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      getToken: () => uni.getStorageSync('token'),
      setToken: (token) => uni.setStorageSync('token', token),
      refreshToken: async () => {
        const res = await uni.request({ url: '/auth/refresh' })
        return { token: res.data.token }
      },
      headerKey: 'Authorization',
      headerValue: (token) => `Bearer ${token}`
    }
  }]
})

刷新失败跳转登录

const request = new Request({
  plugins: [{
    plugin: tokenPlugin,
    config: {
      getToken: () => uni.getStorageSync('token'),
      setToken: (token) => uni.setStorageSync('token', token),
      refreshToken: async () => {
        try {
          const refreshToken = uni.getStorageSync('refreshToken')
          const res = await uni.request({
            url: '/auth/refresh',
            method: 'POST',
            data: { refreshToken }
          })
          return { token: res.data.token }
        } catch {
          // 刷新失败,跳转登录页
          uni.reLaunch({ url: '/pages/login/index' })
          throw new Error('Token 刷新失败')
        }
      }
    }
  }]
})

注意事项

配合 silentCodes

建议将触发刷新的状态码加入 silentCodes,避免同时弹出错误提示:

const request = new Request({
  silentCodes: [409],
  plugins: [{ plugin: tokenPlugin, config: { ... } }]
})

刷新接口避免循环

刷新接口使用独立的 Request 实例或直接使用 uni.request

refreshToken: async () => {
  return new Promise((resolve, reject) => {
    uni.request({
      url: '/auth/refresh',
      method: 'POST',
      success: (res) => resolve({ token: res.data.token }),
      fail: reject
    })
  })
}

请求模式支持

  • ✅ 普通请求(request
  • ✅ 文件上传(upload
  • ❌ 文件下载(download)不处理

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。