UniApp 小程序 - Token 过期自动刷新+失败请求缓存重试方案

一、方案概述

该方案实现了 Token 过期统一拦截、新 Token 仅请求一次、失败请求缓存后重试 的核心功能,解决了多请求并发下 Token 过期导致的重复刷新、请求失败问题,提升了小程序的用户体验和接口调用稳定性。

核心特性

  1. 拦截接口返回的 Token 过期状态(code = -1),统一处理刷新逻辑。
  2. 并发请求下,新 Token 仅获取一次,避免重复调用刷新接口。
  3. 缓存 Token 过期期间的所有失败请求,获取新 Token 后自动重试。
  4. 支持异常处理,刷新失败时清空请求队列并给出友好提示。

二、完整代码实现

import { API_CONFIG } from './api.config.js';
import { BASE_URL } from './config.js';
import { getToken, setStorage, Toast, hideLoading } from './tool.js';
import { wxLogin } from './api.public.js'; // 获取微信登录code

// 全局变量:存储刷新Token的Promise(确保仅请求一次)
let refreshPromise = null;
// 全局变量:缓存Token过期期间的失败请求队列
let requestQueue = [];

/**
 * 自定义封装的请求方法
 * @param {Object} options - 请求配置参数
 * @param {Boolean} skipRefreshLock - 是否跳过刷新锁(刷新Token时的内部请求需设为true,避免循环拦截)
 * @returns {Promise} - 请求结果Promise
 */
const myHttp = (options, skipRefreshLock = false) => {
  return new Promise((resolve, reject) => {
    // 若正在刷新Token且非内部刷新请求,将请求加入缓存队列
    if (!skipRefreshLock && refreshPromise) {
      requestQueue.push({
        options,
        resolve,
        reject
      });
    } else {
      // 直接发起请求
      uni.request(createRequest(options, resolve, reject, skipRefreshLock));
    }
  });
};

/**
 * 构建uni.request所需的配置参数
 * @param {Object} options - 原始请求配置
 * @param {Function} resolve - Promise成功回调
 * @param {Function} reject - Promise失败回调
 * @param {Boolean} skipRefreshLock - 是否跳过刷新锁
 * @returns {Object} - 处理后的uni.request配置
 */
const createRequest = (options, resolve, reject, skipRefreshLock) => {
  const param = dealRequestParams(options);

  return {
    ...param,
    success: (res) => {
      // 接口状态码200(网络请求成功)
      if (res.statusCode == 200) {
        // Token过期(业务状态码-1)
        if (res.data.code == -1) {
          handleTokenExpiration(options, resolve, reject, skipRefreshLock);
        } else {
          // 正常返回,执行成功回调
          resolve(res.data);
          hideLoading();
        }
      } else {
        // 处理HTTP状态码错误(401、404等)
        handleHttpError(res.statusCode, reject);
      }
    },
    fail: (err) => {
      // 网络请求失败(如断网、超时)
      Toast('网络请求出错!');
      hideLoading();
      reject(err);
    }
  };
};

/**
 * 处理Token过期逻辑
 * @param {Object} options - 原始请求配置
 * @param {Function} resolve - Promise成功回调
 * @param {Function} reject - Promise失败回调
 * @param {Boolean} skipRefreshLock - 是否跳过刷新锁
 */
const handleTokenExpiration = (options, resolve, reject, skipRefreshLock) => {
  // 将当前过期请求加入缓存队列
  requestQueue.push({
    options,
    resolve,
    reject
  });

  // 若未在刷新Token,发起刷新请求(确保仅执行一次)
  if (!refreshPromise) {
    refreshPromise = new Promise(async (refreshResolve) => {
      try {
        // 1. 获取微信登录code
        const wxcode = await wxLogin();
        // 2. 调用静默登录接口刷新Token(内部请求,skipRefreshLock=true避免循环拦截)
        const refreshRes = await myHttp({
          api: 'silentLogin',
          method: 'post',
          data: {
            code: wxcode
          }
        }, true);

        // 3. 刷新Token成功(业务状态码1且包含新Token)
        if (refreshRes.code == 1 && refreshRes.data.token) {
          const newToken = refreshRes.data.token;
          // 4. 缓存新Token到本地
          await setStorage('TOKEN', newToken);
          // 5. 重试所有缓存的失败请求
          retryQueuedRequests(newToken);
        } else {
          // 6. 刷新Token失败(如code无效、返回无Token)
          clearRequestQueue('登录状态已失效,请重新登录');
        }
      } catch (error) {
        // 7. 捕获刷新过程中的异常(如接口请求失败)
        clearRequestQueue('Token 刷新失败,请检查网络');
      } finally {
        // 8. 重置刷新Promise,释放锁,允许后续可能的刷新操作
        refreshPromise = null;
        refreshResolve();
      }
    });
  }
};

/**
 * 重试所有缓存的失败请求
 * @param {String} newToken - 新获取的有效Token
 */
const retryQueuedRequests = (newToken) => {
  // 复制请求队列并清空原队列(避免重试过程中新增请求干扰)
  const queue = [...requestQueue];
  requestQueue = [];

  // 遍历队列,逐个重试请求
  queue.forEach(({ options, resolve, reject }) => {
    // 构建新的请求配置,替换为新Token
    const newOptions = {
      ...options,
      header: {
        ...options.header,
        token: newToken
      }
    };
    // 重试请求,结果透传原Promise的回调
    myHttp(newOptions, true).then(resolve).catch(reject);
  });
};

/**
 * 清空请求队列并返回错误信息
 * @param {String} errorMessage - 错误提示信息
 */
const clearRequestQueue = (errorMessage) => {
  // 复制请求队列并清空原队列
  const queue = [...requestQueue];
  requestQueue = [];

  // 遍历队列,逐个执行失败回调
  queue.forEach(({ reject }) => {
    reject(new Error(errorMessage));
  });
};

/**
 * 处理HTTP状态码错误
 * @param {Number} statusCode - HTTP状态码
 * @param {Function} reject - Promise失败回调
 */
const handleHttpError = (statusCode, reject) => {
  let errorMessage = '';
  switch (statusCode) {
    case 401:
      errorMessage = '身份验证失败';
      break;
    case 403:
      errorMessage = '您没有权限执行此操作';
      break;
    case 404:
      errorMessage = '请求的资源不存在';
      break;
    case 500:
      errorMessage = '服务器内部错误';
      break;
    default:
      errorMessage = `网络错误: ${statusCode}`;
  }
  Toast(errorMessage);
  reject(new Error(errorMessage));
};

/**
 * 处理请求参数,格式化uni.request所需配置
 * @param {Object} options - 原始请求配置
 * @returns {Object} - 格式化后的请求参数
 */
const dealRequestParams = (options) => {
  // 拼接请求URL(支持直接传url或通过api配置获取)
  let url = options.url ? options.url : API_CONFIG[options.api];
  // 获取本地缓存的Token
  let token = getToken();
  // 若请求头中自定义了token,优先使用自定义值
  if (options.header?.token) {
    token = options.header.token;
  }

  // 构建默认请求头
  let header = {
    'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
    token
  };

  // 格式化请求方法(默认GET,转为大写)
  options.method = options.method ? options.method.toUpperCase() : 'GET';

  // 若请求数据包含数组,修改content-type为json格式
  if (options.data) {
    for (let i in options.data) {
      if (Array.isArray(options.data[i])) {
        header['content-type'] = 'application/json';
        break;
      }
    }
  }

  // 返回格式化后的请求参数
  return {
    url: BASE_URL + url,
    method: options.method,
    data: options.data || {},
    header: options.header || header
  };
};

// 导出自定义请求方法
export { myHttp };   

三、核心逻辑解析

3.1 全局变量作用(实现“仅刷新一次”核心)

  1. refreshPromise:存储刷新 Token 的 Promise 实例,作为刷新锁
    • null 时:表示当前无刷新操作,可发起新的刷新请求。
    • 不为 null 时:表示正在刷新 Token,后续过期请求直接加入队列,无需重复刷新。
  2. requestQueue:存储 Token 过期期间所有失败的请求,等待新 Token 获取后统一重试。

3.2 核心流程梳理

  1. 正常请求:调用 myHttp(),若未在刷新 Token,直接构建请求参数并发起 uni.request
  2. Token 过期拦截:接口返回 code = -1,将当前请求加入队列,判断是否正在刷新 Token。
  3. 首次刷新 TokenrefreshPromisenull,创建新 Promise,调用 wxLogin() 获取 code,再调用静默登录接口获取新 Token。
  4. 刷新成功处理:缓存新 Token,遍历请求队列,替换 Token 后逐个重试请求,执行原 Promise 回调。
  5. 刷新失败处理:清空请求队列,逐个执行失败回调,给出用户友好提示。
  6. 释放刷新锁:刷新流程(成功/失败)结束后,将 refreshPromise 置为 null,允许后续可能的刷新操作。

3.3 关键方法说明

方法名 核心作用 注意点
myHttp() 对外暴露的请求入口,实现请求队列缓存逻辑 skipRefreshLocktrue 时跳过队列缓存,用于刷新 Token 的内部请求
handleTokenExpiration() 统一处理 Token 过期,控制刷新逻辑和队列缓存 确保 refreshPromise 仅赋值一次,避免并发刷新
retryQueuedRequests() 重试所有缓存请求 先复制队列再清空,避免重试过程中新增请求干扰
clearRequestQueue() 刷新失败时清空队列 遍历队列执行 reject,将错误信息透传给原请求
dealRequestParams() 格式化请求参数 自动适配 form-urlencoded/json 格式,支持自定义 Token

四、使用说明与注意事项

4.1 正常使用

// 引入封装的myHttp方法
import { myHttp } from './http.js';

//或者myHttp挂载到vue或者uni
uni.$http = myHttp;
Vue.prototype.$http = myHttp;

// 发起业务请求
myHttp({
  api: 'userInfo', // 对应API_CONFIG中的配置
  method: 'get',
  data: {
    userId: 123
  }
}).then(res => {
  // 处理接口返回数据
  console.log('用户信息:', res);
}).catch(err => {
  // 处理请求失败(包括Token刷新失败、网络错误等)
  console.error('请求失败:', err);
});

  
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐