tql,寥寥几行,实现无队列无感刷新

前言

可能大家在项目中已经都用上了双 token,可能你的 refresh_token 至今为止都没派上用场。

你可能已经看过了很多篇讲 token 无感刷新的文章了,看完的你可能有三种情况

  1. 不知所云
  2. 又臭又长,懒得看
  3. 管理一个重试队列(queue)

相信聪明又耐心的你们,大多处在第三种情况。

而这篇文章特别短,将在最快的时间内教会你一种 十分简单且更高效 的刷新方式。

无队列无感刷新

要说无队列无感刷新前,要先了解什么是队列式无感刷新,请看下文

队列式 无感刷新机制

定义一个标志位,用于检查是否正在执行刷新行为,再定义一个数组,用来存放 401 的请求,在拦截器中拦截到时去请求刷新,如果标志位为 false,则去请求刷新并将标志位设置为 true、将请求 push 进队列,如果标志位为 true(第二个 401 请求)则直接 push 到队列并返回。等到刷新完成后,遍历重试队列。

它的代码大概长这样:

TypeScript 复制代码
let isRefreshing = false
let requestQueue: ((token: string) => void)[] = []

axiosInstance.interceptors.response.use(undefined, async (error) => {
  const { config, response } = error
  if (response?.status === 401) {
    if (!isRefreshing) {
      isRefreshing = true
      try {
        const token = await refreshToken()
        requestQueue.forEach((cb) => cb(token))
        requestQueue = []
      } catch {
        redirectToLogin()
      } finally {
        isRefreshing = false
      }
    }

    return new Promise((resolve) => {
      requestQueue.push((token) => {
        config.headers.Authorization = 'Bearer ' + token
        resolve(axiosInstance(config))
      })
    })
  }
})

这是一个可用的最简版本,代码已经来到了 27 行,再加一些业务逻辑,这代码已经很丑了。

无队列无感刷新(promise 天然队列)

接下来看无队列的无感刷新,直接看代码:

TypeScript 复制代码
let refreshPromise: Promise<string> | null = null

async function tryRefreshToken(config) {
  if (!refreshPromise) {
    refreshPromise = axios.post('/api/v1/user/token/refresh')
      .catch(()=>{
        redirectToLogin()
      })
      .finally(() => { refreshPromise = null })
  }
  const token = await refreshPromise
  config.headers.Authorization = 'Bearer ' + token
  return axiosInstance(config)
}

哈哈,一共 14 行,实现的功能跟上面是一致的,当请求 401 时,你只需返回这个函数的结果即可,像这样

TypeScript 复制代码
if (error.response.status === 401) {
  return tryRefreshToken(config as AxiosRequestConfig)
}

在这个逻辑下,当多个请求几乎同时返回 401 时:

  • 只会触发一次刷新 token (由 !refreshPromise 控制);

  • 其他请求都会等待这次刷新完成,然后复用新 token 重新发起原请求;

  • 防止并发多次刷新 token (造成 race condition 竞态条件 和资源浪费)。

这就是所谓的:"Token 刷新单例 + 队列重试" 模式

只不过这个队列不用我们手动维护,而是使用了 promise 的机制形成的天然队列,请求会自动在 token 刷新后重新请求。

与常见的队列式方式对比一下

维度 隐式 Promise 方案 显式队列方案
复杂度 中高
语义清晰度 简洁但隐式 明确但冗长
调试难度 容易 稍高
实际效果 ✅ 等价 ✅ 等价

现在看看你项目里要不要换成这种简单高效的刷新方式?我已经用上了,结合我的业务代码也只有这么几行代码

TypeScript 复制代码
// handle 401
async function tryRefreshToken(config: AxiosRequestConfig) {
  if (!refreshPromise) {
    refreshPromise = axios
      .post('/api/v1/user/token/refresh')
      .then((res) => {
        if (res.status !== 200 || res.data.code !== 200) {
          redirectToLogin()
          throw new Error('Failed to refresh token')
        }
        const newToken = Cookies.get('access_token')!
        axiosInstance.defaults.headers.common['Authorization'] = 'Bearer ' + newToken
        return newToken
      })
      .catch((err) => {
        redirectToLogin()
        throw err
      })
      .finally(() => {
        refreshPromise = null
      })
  }

  const token = await refreshPromise
  config.headers!['Authorization'] = 'Bearer ' + token
  eventBus.emit('tokenRefreshed', token)
  return axiosInstance(config)
}

结语

我们常常为了便于思考用显式的结构去模拟隐式的行为,但是维护起来效果却适得其反,而在 Promise 时代,有时候"等待"本身就是最好的队列,感谢你花三分钟看完了我的文章,并学会了新的无队列无感刷新

恩师德莱厄斯(不是被迫的)

相关推荐
boombb1 分钟前
用户反馈入口
前端
im_AMBER3 分钟前
万字长文:手撕JS深浅拷贝完全指南
前端·javascript·面试
还是大剑师兰特5 分钟前
pinia-plugin-persistedstate详解与Vue3使用示例
开发语言·javascript·ecmascript
@大迁世界8 分钟前
20.“可复用组件”具体指的是什么?如何设计与产出这类组件?.
开发语言·前端·javascript·ecmascript
Bigger11 分钟前
第二章:我是如何剖析 Claude Code QueryEngine 与大模型交互机制的
前端·ai编程·源码阅读
FelixBitSoul16 分钟前
彻底吃透 React Hook:它背后的执行模型到底是什么? 🚀
前端
Huanzhi_Lin22 分钟前
Nginx本地资源服务器-常用脚本
服务器·前端·nginx·batch·静态资源服务器
weixin1997010801622 分钟前
《好看视频商品详情页前端性能优化实战》
前端·性能优化·音视频
有意义23 分钟前
深入理解浏览器存储方案:从Cookie到JWT登录认证
前端·面试·浏览器
jiayong2324 分钟前
第 6 课:第二轮真实重构,拆出任务表格组件
前端·重构