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 时代,有时候"等待"本身就是最好的队列,感谢你花三分钟看完了我的文章,并学会了新的无队列无感刷新!

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

相关推荐
红尘散仙6 小时前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
Penge6666 小时前
《Go 后端开发者学 React:组件通信与插槽》
javascript
陈随易7 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
ZhengEnCi7 小时前
Agent 工具怎么折叠海量输出才「爽」:Trae、Claude Code 的渐进式披露设计原理
前端·agent
Penge6668 小时前
《Go 后端开发者学 React:useState》
javascript
Penge6668 小时前
《Go 后端开发者学 React:数据渲染》
javascript
IT_陈寒11 小时前
Redis内存暴涨,没想到是AOF重写捣的鬼
前端·人工智能·后端
ikoala11 小时前
让 Codex 自己查账:一个省 token 的小技巧
前端·后端·ai编程
ikoala11 小时前
AI 写页面像说明书,修 Bug 像贴膏药?把这两句话写进 AGENTS.md
前端·javascript·ai编程
Penge66611 小时前
《Go 后端开发者学 TypeScript:ES Module了解》
javascript