UniApp HTTP 请求封装:优雅实现 503 自动重试

介绍说明:

这是一个为 UniApp 框架设计的 增强型 HTTP 请求封装模块 (基于 uni.request),使用 TypeScript 编写。它的核心目标是简化网络请求的处理,并自动、优雅地处理后端服务临时不可用 (HTTP 503) 的情况,从而提高应用的健壮性和用户体验。

当你调用后端接口时,偶尔可能会遇到服务器因为维护、过载或其他临时性问题而返回 503 状态码。通常,这意味着稍后重试请求可能会成功。本模块会自动捕获 503 错误,并根据预设(或自定义)的次数和延迟时间进行自动重试,避免了在每个请求中手动编写重试逻辑的繁琐工作。

主要特性:

  • 🚀 503 自动重试 : 无需手动干预,自动处理 503 Service Unavailable 错误,并在指定延迟后重试。
  • 🔧 配置灵活: 可以全局设置默认的重试次数和延迟时间,也可以在单次请求中覆盖这些配置。
  • ✨ Promise 化 : 完全基于 Promise,完美支持 async/await 语法,让异步代码更清晰。
  • 💡 辅助函数 : 提供 httpGet, httpPost, httpPut, httpDelete 等常用方法的快捷方式,简化调用。
  • 🛡️ 结构化错误处理 : 失败时 reject 一个包含详细信息的 HttpRequestError 对象(包括消息、原始错误、状态码、是否网络错误等),便于精细化错误处理。
  • 💬 可控错误提示 : 请求失败时默认弹出 uni.showToast 提示,但可通过选项 (hideErrorToast: true) 禁用,方便自定义UI反馈。
  • 🔒 类型安全 : 使用 TypeScript 泛型 <T>,允许你指定期望的响应数据类型,在编译时提供更好的类型检查。
  • 🧩 独立纯净: 不依赖外部状态管理库(如 Pinia/Vuex),易于集成到任何 UniApp 项目中。

适用场景:

  • 任何需要与后端 API 交互的 UniApp 项目。
  • 希望提高应用对后端临时故障容错能力的应用。
  • 寻求更简洁、统一的网络请求处理方式的开发者。

如何使用:

将此模块(例如 request.ts)放置在你的项目工具(utils)目录下,然后在需要发起网络请求的页面或组件中导入 http 或其辅助函数 (httpGet, httpPost 等) 进行调用即可。

typescript 复制代码
// 示例:
import { http } from '@/utils/request'; // 假设你放在 utils 目录下

async function fetchData() {
  try {
    const data = await http.get<MyDataType>('/api/data');
    console.log(data);
  } catch (error) {
    console.error('请求失败:', error);
    // 处理错误...
  }
}

完整代码:

typescript 复制代码
// src/utils/request.ts (或其他你喜欢的文件路径)

// --- 配置常量 ---
const DEFAULT_MAX_RETRIES = 2; // 默认最大重试次数 (总尝试次数 = 1 + 重试次数)
const DEFAULT_RETRY_DELAY = 1000; // 默认两次重试之间的延迟时间 (毫秒)

// --- TypeScript 接口定义 ---

/**
 * 扩展 UniApp 的 RequestOptions,增加自定义的重试配置
 */
interface RetryRequestOptions extends UniApp.RequestOptions {
  /** 针对 503 错误的最大重试次数 (默认: 2) */
  retries?: number;
  /** 每次重试之间的延迟时间,单位毫秒 (默认: 1000) */
  retryDelay?: number;
  /** 设置为 true 可隐藏自动弹出的错误提示 Toast (默认: false) */
  hideErrorToast?: boolean;
}

/**
 * 定义一个结构化的 HTTP 请求错误对象,用于 Promise 的 reject
 */
interface HttpRequestError {
  /** 错误消息文本 */
  message: string;
  /** uni-app 返回的原始错误对象或请求失败的响应对象 */
  originalError: UniApp.GeneralCallbackResult | UniApp.RequestSuccessCallbackResult;
  /** 标志是否为网络连接错误 (即 fail 回调触发) */
  isNetworkError?: boolean;
  /** HTTP 状态码 (如果是非 2xx 的成功响应) */
  statusCode?: number;
}

// --- 核心 HTTP 请求函数 ---

/**
 * 发起一个 HTTP 请求 (基于 uni.request),并带有 503 自动重试逻辑。
 *
 * @template T - 期望的响应数据 `data` 的类型,默认为 `any`。
 * @param {RetryRequestOptions} options - 请求配置,包含 uni.request 的标准选项以及自定义的重试选项。
 * @returns {Promise<T>} - 返回一个 Promise。成功时 resolve 响应的 `data` 部分;失败时 reject 一个 `HttpRequestError` 对象。
 */
export const http = <T = any>(options: RetryRequestOptions): Promise<T> => {
  // 解构选项,分离出重试配置和标准的 uni.request 配置
  const {
    retries = DEFAULT_MAX_RETRIES,        // 获取重试次数,若未提供则使用默认值
    retryDelay = DEFAULT_RETRY_DELAY,     // 获取重试延迟,若未提供则使用默认值
    hideErrorToast = false,             // 获取是否隐藏错误提示的标志
    ...requestOptions                   // 剩余的选项作为 uni.request 的标准参数
  } = options;

  /**
   * 内部函数,用于执行单次请求尝试
   * @param currentAttempt - 当前是第几次尝试 (从 0 开始)
   */
  const attemptRequest = (currentAttempt: number): Promise<T> => {
    // 返回一个新的 Promise 来包装单次 uni.request 调用
    return new Promise<T>((resolve, reject) => {
      uni.request({
        ...requestOptions, // 传入标准的 uni.request 选项 (url, method, data, header 等)

        // 请求成功的回调 (HTTP 状态码不一定是 2xx)
        success(res) {
          // --- 成功情况 (状态码 200-299) ---
          if (res.statusCode >= 200 && res.statusCode < 300) {
            // 假设服务器返回的有效数据在 res.data 中
            resolve(res.data as T); // 使用 T 类型断言
            return; // 成功,退出回调
          }

          // --- 需要重试的情况 (状态码 503 且 未达到最大重试次数) ---
          if (res.statusCode === 503 && currentAttempt < retries) {
            const attemptNum = currentAttempt + 1; // 计算下一次尝试的序号
            console.warn(
              `[HTTP] 请求失败,状态码 503。将在 ${retryDelay}ms 后进行第 ${attemptNum}/${retries} 次重试... URL: ${requestOptions.url}`
            );
            // 设置延迟执行下一次尝试
            setTimeout(() => {
              // 递归调用 attemptRequest,并将结果通过 then/catch 传递给外层 Promise
              attemptRequest(attemptNum).then(resolve).catch(reject);
            }, retryDelay);
            return; // 等待重试,退出当前回调
          }

          // --- 其他失败情况 (非 2xx 状态码,或 503 重试次数已用尽) ---
          // 构造错误消息
          const errorMessage = `请求失败 [状态码: ${res.statusCode}]`;
          console.error(`[HTTP] 请求错误 ${res.statusCode}: ${requestOptions.url}`, res);
          // 如果用户没有选择隐藏提示,则显示 Toast
          if (!hideErrorToast) {
            uni.showToast({
              icon: 'none',
              // 尝试从响应体中获取更具体的错误信息,否则使用通用信息
              title: (res.data as any)?.message || errorMessage,
              duration: 2000,
            });
          }
          // 用结构化的错误对象 reject Promise
          reject({
            message: errorMessage,
            originalError: res, // 保留原始响应对象
            statusCode: res.statusCode, // 记录状态码
          } as HttpRequestError);
        },

        // 请求失败的回调 (网络层面的错误,例如 DNS 解析失败、网络不可达等)
        fail(err) {
          // --- 网络错误情况 ---
          const errorMessage = '网络连接错误,请检查网络设置或稍后重试';
          console.error(`[HTTP] 网络错误: ${requestOptions.url}`, err);
          // 如果用户没有选择隐藏提示,则显示 Toast
          if (!hideErrorToast) {
            uni.showToast({
              icon: 'none',
              title: errorMessage,
              duration: 2000,
            });
          }
          // 用结构化的错误对象 reject Promise
          reject({
            message: errorMessage,
            originalError: err, // 保留原始错误对象
            isNetworkError: true, // 标记为网络错误
          } as HttpRequestError);
        },

        // complete 回调在 success 或 fail 后都会执行,此处 Promise 的 resolve/reject 已处理最终状态,故无需操作
        // complete() { }
      });
    });
  };

  // 发起第一次请求尝试 (currentAttempt = 0)
  return attemptRequest(0);
};

// --- 便捷的辅助函数 ---

/** 定义辅助函数的选项类型,排除掉 url, method, data 这三个由辅助函数自身处理的参数 */
type HttpHelperOptions = Omit<RetryRequestOptions, 'url' | 'method' | 'data'>;

/**
 * 发起 GET 请求 (带 503 重试)
 * @template T - 期望的响应数据类型
 * @param url - 请求地址
 * @param data - 查询参数 (uni.request 中 GET 请求的 query 参数通常放在 data 对象里)
 * @param options - 其他配置,如 headers, retries, hideErrorToast 等
 */
export const httpGet = <T = any>(
  url: string,
  data?: UniApp.RequestOptions['data'],
  options?: HttpHelperOptions
): Promise<T> => {
  return http<T>({
    url,
    data, // 对于 GET, uni.request 将 data 作为 query 参数附加到 URL
    method: 'GET',
    ...options, // 合并其他选项
  });
};

/**
 * 发起 POST 请求 (带 503 重试)
 * @template T - 期望的响应数据类型
 * @param url - 请求地址
 * @param data - 请求体数据
 * @param options - 其他配置
 */
export const httpPost = <T = any>(
  url: string,
  data?: UniApp.RequestOptions['data'],
  options?: HttpHelperOptions
): Promise<T> => {
  return http<T>({
    url,
    data, // 请求体
    method: 'POST',
    ...options,
  });
};

/**
 * 发起 PUT 请求 (带 503 重试)
 * @template T - 期望的响应数据类型
 * @param url - 请求地址
 * @param data - 请求体数据
 * @param options - 其他配置
 */
export const httpPut = <T = any>(
  url: string,
  data?: UniApp.RequestOptions['data'],
  options?: HttpHelperOptions
): Promise<T> => {
  return http<T>({
    url,
    data, // 请求体
    method: 'PUT',
    ...options,
  });
};

/**
 * 发起 DELETE 请求 (带 503 重试)
 * @template T - 期望的响应数据类型
 * @param url - 请求地址
 * @param data - 查询参数或请求体 (根据后端 API 设计,DELETE 有时也用 data 传递参数)
 * @param options - 其他配置
 */
export const httpDelete = <T = any>(
  url: string,
  data?: UniApp.RequestOptions['data'],
  options?: HttpHelperOptions
): Promise<T> => {
  return http<T>({
    url,
    data,
    method: 'DELETE',
    ...options,
  });
};

// (可选) 将辅助函数附加到主 http 函数上,提供类似 axios 的调用风格
http.get = httpGet;
http.post = httpPost;
http.put = httpPut;
http.delete = httpDelete;

// --- 使用示例 ---
/*
// 定义一个接口来描述期望获取的数据结构
interface UserInfo {
  id: number;
  name: string;
  email?: string;
}

async function fetchUserData() {
  console.log('开始获取用户数据...');
  try {
    // 使用 http.get,并指定泛型类型为 UserInfo
    // 第二个参数是 GET 请求的查询参数
    // 第三个参数可以传递额外的配置,比如自定义请求头
    const userInfo = await http.get<UserInfo>(
      '/api/user/profile', // 假设的 API 地址
      { userId: 123 },     // 查询参数: /api/user/profile?userId=123
      {
        headers: { 'Authorization': 'Bearer YOUR_TOKEN' },
        // retries: 1, // 可以覆盖默认的重试次数
        // hideErrorToast: true, // 可以在单次调用中禁用 Toast
      }
    );
    console.log('成功获取用户数据:', userInfo);
    // 可以安全地访问 userInfo.id, userInfo.name 等属性

  } catch (error: any) {
    // 捕获错误,类型断言为我们定义的 HttpRequestError
    const err = error as HttpRequestError;
    console.error('获取用户数据失败:', err.message); // 输出友好的错误信息

    // 可以根据错误类型做进一步处理
    if (err.isNetworkError) {
      console.log('失败原因:网络连接问题。');
    } else {
      console.log(`失败原因:服务器返回状态码 ${err.statusCode}。`);
      // 可以查看原始错误对象获取更多细节
      // console.log('原始错误详情:', err.originalError);
    }
  }
}

async function updateUserProfile() {
  console.log('开始更新用户资料...');
  try {
    // 使用 http.post 发送数据
    const result = await http.post<{ success: boolean; message: string }>(
      '/api/user/update', // 假设的更新 API
      { name: '新名字', email: '[email protected]' }, // 请求体数据
      { headers: { 'Authorization': 'Bearer YOUR_TOKEN' } }
    );
    console.log('更新结果:', result); // 查看服务器返回的结果

  } catch (error: any) {
    const err = error as HttpRequestError;
    console.error('更新用户资料失败:', err.message);
    // 处理更新失败的情况...
  }
}

// 调用示例函数
fetchUserData();
// updateUserProfile();

*/
  1. 核心 http 函数:

    • 封装了 UniApp 的 uni.request API。
    • 内置 503 重试 : 当请求收到 503 Service Unavailable 状态码时,会自动进行重试。
    • 可配置 : 你可以通过 options 对象自定义重试次数 (retries) 和重试间的延迟时间 (retryDelay)。
    • Promise 化 : 返回 Promise,方便使用 async/await.then()/.catch() 处理异步操作。
    • 错误处理 : 网络错误或最终失败(包括重试用尽后的 503 或其他 HTTP 错误)会 reject 一个包含详细信息的 HttpRequestError 对象。
    • 错误提示 : 默认情况下,请求失败时会弹出 uni.showToast 提示,但可以通过 hideErrorToast: true 选项禁用。
    • 泛型支持 : 使用 <T> 泛型,允许你指定期望的响应数据 data 的类型,增强代码类型安全。
  2. 辅助函数 (httpGet, httpPost, httpPut, httpDelete):

    • 基于核心 http 函数,为常见的 HTTP 方法(GET, POST, PUT, DELETE)提供了便捷的快捷方式。
    • 简化了调用,只需传入 URL、数据(可选)和额外配置(可选)。
  3. 独立性:

    • 这个封装是自包含的,不依赖外部的状态管理库(如 Pinia/Vuex)或全局计数器。
  4. 结构化错误对象 (HttpRequestError):

    • 当请求失败时,Promise 会被拒绝(reject),并传递一个 HttpRequestError 对象。这个对象包含了错误信息 (message)、原始的 uni-app 错误或响应对象 (originalError)、可选的状态码 (statusCode) 以及是否为网络错误的标志 (isNetworkError),方便进行更细致的错误处理。

总之,这个模块旨在提供一个更健壮、更易于使用的 HTTP 请求层,特别是在需要处理后端服务临时不可用(503)的情况下。

相关推荐
05091517 分钟前
测试基础笔记第四天(html)
前端·笔记·html
聪明的墨菲特i1 小时前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
时光少年1 小时前
Android 副屏录制方案
android·前端
拉不动的猪1 小时前
v2升级v3需要兼顾的几个方面
前端·javascript·面试
时光少年1 小时前
Android 局域网NIO案例实践
android·前端
半兽先生1 小时前
VueDOMPurifyHTML 防止 XSS(跨站脚本攻击) 风险
前端·xss
冴羽1 小时前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
Jackson__1 小时前
面试官:谈一下在 ts 中你对 any 和 unknow 的理解
前端·typescript
zpjing~.~2 小时前
css 二维码始终显示在按钮的正下方,并且根据不同的屏幕分辨率自动调整位置
前端·javascript·html
红虾程序员2 小时前
Linux进阶命令
linux·服务器·前端