封装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 重新发起了一次它最初失败的请求。
相关推荐
小兵张健10 小时前
价值1000的 AI 工作流:Codex 通用前端协作模式
前端·aigc·ai编程
sunny_10 小时前
面试踩大坑!同一段 Node.js 代码,CJS 和 ESM 的执行顺序居然是反的?!99% 的人都答错了
前端·面试·node.js
拉不动的猪10 小时前
移动端调试工具VConsole初始化时的加载阻塞问题
前端·javascript·微信小程序
ayqy贾杰12 小时前
Agent First Engineering
前端·vue.js·面试
IT_陈寒12 小时前
SpringBoot实战:5个让你的API性能翻倍的隐藏技巧
前端·人工智能·后端
iceiceiceice13 小时前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
大金乄13 小时前
封装一个vue2的elementUI 表格组件(包含表格编辑以及多级表头)
前端·javascript
葡萄城技术团队14 小时前
【性能优化篇】面对万行数据也不卡顿?揭秘协同服务器的“片段机制 (Fragments)”
前端
程序员阿峰14 小时前
2026前端必备:TensorFlow.js,浏览器里的AI引擎,不写Python也能玩转智能
前端
Jans14 小时前
Shipfe — Rust 写的前端静态部署工具:一条命令上线 + 零停机 + 可回滚 + 自动清理
前端