封装fetch请求 带401请求重试

js 复制代码
import { getReToken, getToken, setReToken, setToken, removeReToken, removeToken } from '@/utils/auth';
import useEnterStore from '@/store/modules/enterprise';
import { refreshToken } from '@/api/login';

// --- 状态和配置  ---
let isRefreshing = ref(false);
let failedQueue = [];

// --- 队列处理器 ---
const processQueue = (error, token = null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });
  failedQueue = [];
};

// --- 核心 fetch 封装 ---
async function fetchWithAuth(url, options = {}) {
  const enterStore = useEnterStore();
  // 1. 获取 token 并添加到请求头
  const headers = new Headers(options.headers || {});
  if (getToken()) {
    headers.set('Authorization', `Bearer ${getToken()}`);
  }
  options.headers = headers;
  // 2. 发起原始请求
  const response = await fetch(url, options);
  // 3. 检查是否是 token 过期错误 (401)
  if (response.status === 401) {
    if (isRefreshing.value) {
      // 如果已经在刷新 token,则将当前请求加入队列等待
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then((newToken) => {
        // 等待刷新成功后,用新 token 重试
        headers.set('Authorization', `Bearer ${newToken}`);
        options.headers = headers;
        return fetch(url, options);
      });
    }
    isRefreshing.value = true;
    try {
      if (!getReToken()) {
        // 换取新token失败,跳转到登出页面
        location.href = '/app/mobile/logout';
        return Promise.reject(new Error('Session expired. No refresh token.'));
      }
      // 4. 发起刷新 token 的请求
      let res = await refreshToken(enterStore.enterInfo.authAddress, enterStore.enterInfo.id, enterStore.enterInfo.clientId);
      const { access_token, expires_in, refresh_token, refresh_expires_in } = res;
      // 5. 更新 token 并处理等待队列
      setToken(access_token, expires_in);
      setReToken(refresh_token, refresh_expires_in);
      processQueue(null, access_token);
      // 6. 用新 token 重试原始请求
      headers.set('Authorization', `Bearer ${access_token}`);
      options.headers = headers;
      return fetch(url, options);
    } catch (error) {
      // 刷新失败,登出并拒绝所有等待的请求
      processQueue(error, null);
      location.href = '/app/mobile/logout';
      return Promise.reject(error);
    } finally {
      isRefreshing.value = false;
    }
  }
  // 如果不是 401 错误,直接返回响应
  return response;
}

// --- 导出的 Composable 函数 ---

export function useApi() {
  // 返回一个可以直接使用的 fetch 实例
  return {
    apiFetch: fetchWithAuth,
  };
}

关键代码块详解

1. 状态和队列 (isRefreshing, failedQueue)

JavaScript

ini 复制代码
// --- 状态和配置  ---
let isRefreshing = ref(false);
let failedQueue = [];
  • isRefreshing: 这是一个"锁"或"标志位"。它的作用是告诉整个应用:"当前是否已经有请求正在刷新 token?"。ref(false) 意味着它是一个响应式变量,虽然在这里主要用于逻辑判断。
  • failedQueue: 这是一个"等待队列"。当 isRefreshingtrue 时,所有后续失败的请求都会被暂时存放到这个队列里,等待被"唤醒"。

2. 队列处理器 (processQueue)

JavaScript

javascript 复制代码
// --- 队列处理器 ---
const processQueue = (error, token = null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error); // 如果刷新失败,就通知所有等待的请求:你们也失败了
    } else {
      prom.resolve(token); // 如果刷新成功,就用新 token "唤醒"所有等待的请求
    }
  });
  failedQueue = []; // 处理完后清空队列
};

这个函数是 failedQueue 的配套工具。它的职责是在 token 刷新操作结束后(无论成功或失败),去处理所有在队列中等待的请求。

3. 核心请求函数 (fetchWithAuth)

  • 步骤 1 & 2: 正常请求

    JavaScript

    javascript 复制代码
    const headers = new Headers(options.headers || {});
    if (getToken()) {
      headers.set('Authorization', `Bearer ${getToken()}`);
    }
    // ...
    const response = await fetch(url, options);

    这部分很简单:为请求附加 Authorization 头,然后正常发送。

  • 步骤 3: 拦截 401 错误

    JavaScript

    ini 复制代码
    if (response.status === 401) {
      // ...
    }

    这是整个逻辑的触发点。

  • 并发处理逻辑

    JavaScript

    javascript 复制代码
    if (isRefreshing.value) {
      // 如果已经在刷新 token,则将当前请求加入队列等待
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then((newToken) => {
        // ...用新 token 重试
      });
    }
    isRefreshing.value = true; // 第一个失败的请求,加锁!
    • 当一个请求收到 401 时,它首先检查 isRefreshing
    • 如果为 true,说明"有人已经在路上去拿新钥匙了",于是它就通过 new Promise把自己挂起,并将 resolvereject 函数存入 failedQueue,然后安静地等待。
    • 如果为 false,说明它是第一个发现问题的请求。它会立刻把 isRefreshing 设为 true,相当于"锁门",防止其他人重复去拿钥匙。然后它继续往下执行刷新 token 的逻辑。
  • 步骤 4: 刷新 Token

    JavaScript

    csharp 复制代码
    try {
      // ...
      let res = await refreshToken(...);
      // ...
    } catch (error) {
      // ...
    } finally {
      isRefreshing.value = false; // 无论成功失败,最后都要"开锁"
    }
    • 这个 try...catch...finally 结构非常健壮。
    • 它调用 refreshToken API。如果成功,就继续往下走。
    • 如果 refreshToken 本身也失败了(例如 refresh token 也过期了),catch 块会执行,处理所有等待的请求(通知它们失败),然后强制用户登出。
    • finally 确保了在所有操作结束后,isRefreshing 这个"锁"一定会被重置为 false,以便下次能正常工作。
  • 步骤 5 & 6: 更新与重试

    JavaScript

    scss 复制代码
    // 5. 更新 token 并处理等待队列
    setToken(access_token, expires_in);
    setReToken(refresh_token, refresh_expires_in);
    processQueue(null, access_token); // "唤醒"所有等待的请求
    
    // 6. 用新 token 重试原始请求
    headers.set('Authorization', `Bearer ${access_token}`);
    return fetch(url, options);
    • 刷新成功后,它会用新的 token 更新本地存储。
    • 调用 processQueue,把新的 access_token 传递给所有在队列里等待的 Promise,触发它们的 .then((newToken) => ...) 逻辑,让它们也用新 token 重新发起请求。
    • 最后,这个"领头"的请求自己也用新 token 重新发起了一次它最初失败的请求。
相关推荐
foundbug9994 分钟前
Modbus协议C语言实现(易于移植版本)
java·c语言·前端
Luna-player5 分钟前
在前端中list.map的用法
前端·数据结构·list
用户47949283569159 分钟前
面试官问 React Fiber,这一篇文章就够了
前端·javascript·react.js
LYFlied21 分钟前
【一句话概述】Webpack、Vite、Rollup 核心区别
前端·webpack·node.js·rollup·vite·打包·一句话概述
reddingtons32 分钟前
PS 参考图像:线稿上色太慢?AI 3秒“喂”出精细厚涂
前端·人工智能·游戏·ui·aigc·游戏策划·游戏美术
一水鉴天34 分钟前
整体设计 定稿 之23+ dashboard.html 增加三层次动态记录体系仪表盘 之2 程序 (Q199 之2) (codebuddy)
开发语言·前端·javascript
刘发财36 分钟前
前端一行代码生成数千页PDF,dompdf.js新增分页功能
前端·typescript·开源
_请输入用户名40 分钟前
Vue 3 源码项目结构详解
前端·vue.js
少卿1 小时前
Next.js 国际化实现方案详解
前端·next.js
掘金挖土1 小时前
手摸手快速搭建 Vue3 + ElementPlus 后台管理系统模板,使用 JavaScript
前端·javascript