封装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 重新发起了一次它最初失败的请求。
相关推荐
独立开阀者_FwtCoder16 分钟前
Vue 抛弃虚拟 DOM,底层到底换成啥了?怎么更新 DOM?
前端·面试·github
望获linux32 分钟前
【实时Linux实战系列】实时任务与信号处理
linux·开发语言·前端·数据库·chrome·操作系统·嵌入式软件
ConardLi41 分钟前
爆改最近超火的 Gemini CLI,让其支持自定义大模型 + 代码引入!
前端·人工智能·后端
fly spider1 小时前
15.缓存过期淘汰策略
前端·缓存·bootstrap
小飞大王6661 小时前
React基础(1)
前端·javascript·react.js
盛码笔记1 小时前
react(基础篇)
前端·javascript·react.js
默默地离开1 小时前
React 中的 useRef 与 forwardRef:深入理解与实战应用
前端·react.js
晨晖21 小时前
前端,demo操作,增删改查,to do list小项目
前端·javascript·list
枣把儿2 小时前
Vibe Coding 一天内完成图片编辑工具( Trae 又行了?
前端·trae