HarmonyOS应用<节气通>开发第25篇:HTTP请求封装

引言

HTTP请求是应用开发中不可或缺的部分。本文将介绍如何封装一个统一的HTTP请求工具类,包括:

  • 请求拦截器
  • 响应拦截器
  • 统一错误处理
  • 请求取消机制
  • 请求重试机制

通过本文,你将掌握如何构建一个健壮的HTTP请求封装。


学习目标

完成本文后,你将能够:

  • ✅ 封装HTTP请求工具类
  • ✅ 添加请求/响应拦截器
  • ✅ 实现统一错误处理
  • ✅ 添加请求取消机制
  • ✅ 实现请求重试

需求分析

功能模块设计

模块 功能描述 技术要点
请求封装 统一请求入口 Promise封装
请求拦截器 请求前处理 Token注入、参数处理
响应拦截器 响应后处理 数据解析、错误处理
错误处理 统一错误处理 错误类型分类、提示
请求取消 取消正在进行的请求 AbortController
请求重试 失败自动重试 重试次数配置

技术原理深度解析

HTTP协议基础

HTTP(Hypertext Transfer Protocol)是一种无状态的应用层协议,用于在客户端和服务器之间传输超文本数据。

HTTP请求结构:

复制代码
GET /api/articles?page=1 HTTP/1.1
Host: api.example.com
Authorization: Bearer token123
Content-Type: application/json
User-Agent: JieQiTong/1.0.0

(请求体,POST/PUT请求时存在)

HTTP响应结构:

复制代码
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1234
Date: Wed, 29 May 2024 10:00:00 GMT

{"code":200,"data":[...],"message":"success"}

HTTP状态码分类:

状态码范围 含义 典型场景
1xx 信息响应 请求已接收,继续处理
2xx 成功 请求已成功处理
3xx 重定向 需要进一步操作
4xx 客户端错误 请求有问题
5xx 服务器错误 服务器处理失败

常用状态码详解:

  • 200 OK - 请求成功
  • 201 Created - 资源创建成功
  • 400 Bad Request - 请求参数错误
  • 401 Unauthorized - 未授权,Token无效或过期
  • 403 Forbidden - 禁止访问,权限不足
  • 404 Not Found - 资源不存在
  • 500 Internal Server Error - 服务器内部错误

拦截器机制原理

拦截器(Interceptor)是一种AOP(面向切面编程)思想的实现,允许在请求发送前和响应返回后插入自定义逻辑。

拦截器执行流程:

复制代码
请求发起
    ↓
请求拦截器1 → 请求拦截器2 → ... → 请求拦截器N
    ↓
发送HTTP请求
    ↓
响应拦截器1 → 响应拦截器2 → ... → 响应拦截器N
    ↓
返回结果给调用者

请求拦截器常见用途:

  1. Token注入 - 在请求头中添加Authorization
  2. 请求参数统一处理 - 添加公共参数、签名
  3. 请求日志记录 - 记录请求信息用于调试
  4. 请求超时设置 - 统一设置超时时间

响应拦截器常见用途:

  1. 统一错误处理 - 处理Token过期、权限不足
  2. 数据格式转换 - 统一处理响应格式
  3. 响应日志记录 - 记录响应信息
  4. 缓存策略 - 对GET请求进行缓存

请求取消机制原理

AbortController是Web API提供的取消请求机制,通过AbortSignal控制请求的取消。

工作原理:

  1. 创建AbortController实例
  2. 将controller.signal传递给fetch请求
  3. 调用controller.abort()取消请求
  4. 请求会抛出AbortError异常

应用场景:

  • 页面切换时取消未完成的请求
  • 连续请求时取消前一个请求(防抖)
  • 用户主动取消操作

请求重试机制原理

请求重试用于在网络不稳定时自动重试请求,提高系统容错能力。

重试策略:

  1. 固定间隔重试 - 每次重试间隔相同时间
  2. 指数退避 - 重试间隔呈指数增长
  3. 随机退避 - 在一定范围内随机选择间隔

指数退避公式:

复制代码
delay = baseDelay * (2^retryCount) + random(0, jitter)

重试条件:

  • 网络错误(NETWORK_ERROR)
  • 服务器错误(5xx状态码)
  • 特定的客户端错误(如408超时)

核心实现

步骤1: HTTP请求工具类封装

typescript 复制代码
// utils/HttpService.ets

import prompt from '@ohos.prompt';

/**
 * HTTP请求工具类
 */
export class HttpService {
  // 基础URL
  private baseUrl: string = '';
  
  // 请求超时时间(毫秒)
  private timeout: number = 30000;
  
  // 最大重试次数
  private maxRetries: number = 3;
  
  // 请求拦截器列表
  private requestInterceptors: Array<RequestInterceptor> = [];
  
  // 响应拦截器列表
  private responseInterceptors: Array<ResponseInterceptor> = [];
  
  // 存储正在进行的请求
  private pendingRequests: Map<string, AbortController> = new Map();
  
  /**
   * 构造函数
   */
  constructor(baseUrl: string = '') {
    this.baseUrl = baseUrl;
  }
  
  /**
   * 添加请求拦截器
   */
  addRequestInterceptor(interceptor: RequestInterceptor): void {
    this.requestInterceptors.push(interceptor);
  }
  
  /**
   * 添加响应拦截器
   */
  addResponseInterceptor(interceptor: ResponseInterceptor): void {
    this.responseInterceptors.push(interceptor);
  }
  
  /**
   * GET请求
   */
  async get<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
    return this.request<T>('GET', url, undefined, params);
  }
  
  /**
   * POST请求
   */
  async post<T>(url: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
    return this.request<T>('POST', url, data);
  }
  
  /**
   * PUT请求
   */
  async put<T>(url: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
    return this.request<T>('PUT', url, data);
  }
  
  /**
   * DELETE请求
   */
  async delete<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
    return this.request<T>('DELETE', url, undefined, params);
  }
  
  /**
   * 统一请求方法
   */
  private async request<T>(
    method: HttpMethod,
    url: string,
    data?: Record<string, any>,
    params?: Record<string, any>
  ): Promise<HttpResponse<T>> {
    // 构建完整URL
    const fullUrl = this.buildUrl(url, params);
    
    // 生成请求标识
    const requestKey = `${method}_${fullUrl}`;
    
    // 取消之前的相同请求
    this.cancelRequest(requestKey);
    
    // 创建AbortController
    const controller = new AbortController();
    this.pendingRequests.set(requestKey, controller);
    
    try {
      // 应用请求拦截器
      const config: RequestConfig = {
        method,
        url: fullUrl,
        headers: {},
        data,
        timeout: this.timeout
      };
      
      for (const interceptor of this.requestInterceptors) {
        await interceptor(config);
      }
      
      // 设置默认headers
      if (!config.headers['Content-Type']) {
        config.headers['Content-Type'] = 'application/json';
      }
      
      // 发送请求
      const response = await fetch(fullUrl, {
        method,
        headers: config.headers,
        body: data ? JSON.stringify(data) : undefined,
        signal: controller.signal
      });
      
      // 应用响应拦截器
      for (const interceptor of this.responseInterceptors) {
        await interceptor(response);
      }
      
      // 解析响应
      const result = await this.parseResponse<T>(response);
      
      return result;
    } catch (error) {
      throw this.handleError(error);
    } finally {
      // 移除请求记录
      this.pendingRequests.delete(requestKey);
    }
  }
  
  /**
   * 构建URL
   */
  private buildUrl(url: string, params?: Record<string, any>): string {
    let fullUrl = this.baseUrl ? `${this.baseUrl}${url}` : url;
    
    if (params) {
      const queryString = new URLSearchParams(params).toString();
      if (queryString) {
        fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryString;
      }
    }
    
    return fullUrl;
  }
  
  /**
   * 解析响应
   */
  private async parseResponse<T>(response: Response): Promise<HttpResponse<T>> {
    let data: any;
    
    try {
      data = await response.json();
    } catch {
      data = await response.text();
    }
    
    if (response.ok) {
      return {
        success: true,
        data,
        status: response.status,
        message: 'success'
      };
    } else {
      throw {
        success: false,
        data,
        status: response.status,
        message: data?.message || '请求失败'
      };
    }
  }
  
  /**
   * 处理错误
   */
  private handleError(error: any): HttpError {
    if (error.name === 'AbortError') {
      return {
        success: false,
        code: 'REQUEST_CANCELLED',
        message: '请求已取消',
        data: null
      };
    }
    
    if (!error.success) {
      return {
        success: false,
        code: error.status?.toString() || 'UNKNOWN',
        message: error.message || '请求失败',
        data: error.data
      };
    }
    
    return {
      success: false,
      code: 'NETWORK_ERROR',
      message: '网络异常,请检查网络连接',
      data: null
    };
  }
  
  /**
   * 取消请求
   */
  cancelRequest(key: string): void {
    const controller = this.pendingRequests.get(key);
    if (controller) {
      controller.abort();
      this.pendingRequests.delete(key);
    }
  }
  
  /**
   * 取消所有请求
   */
  cancelAllRequests(): void {
    this.pendingRequests.forEach((controller) => {
      controller.abort();
    });
    this.pendingRequests.clear();
  }
}

// 类型定义
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

interface RequestConfig {
  method: HttpMethod;
  url: string;
  headers: Record<string, string>;
  data?: Record<string, any>;
  timeout: number;
}

interface HttpResponse<T> {
  success: boolean;
  data: T;
  status: number;
  message: string;
}

interface HttpError {
  success: boolean;
  code: string;
  message: string;
  data: any;
}

type RequestInterceptor = (config: RequestConfig) => Promise<void> | void;
type ResponseInterceptor = (response: Response) => Promise<void> | void;

/**
 * 创建默认的HTTP服务实例
 */
export const createHttpService = (baseUrl: string = '') => {
  const service = new HttpService(baseUrl);
  
  // 添加默认请求拦截器 - 注入Token
  service.addRequestInterceptor(async (config) => {
    const token = await getToken();
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
  });
  
  // 添加默认响应拦截器 - 处理token过期
  service.addResponseInterceptor(async (response) => {
    if (response.status === 401) {
      // Token过期,跳转到登录页
      prompt.showToast({ message: '登录已过期,请重新登录' });
      // 清除本地token
      await clearToken();
      // 跳转到登录页
      router.pushUrl({ url: 'pages/Login' });
    }
  });
  
  return service;
};

// Mock函数
async function getToken(): Promise<string | null> {
  return null;
}

async function clearToken(): Promise<void> {}
</script>

设计要点:

  • 使用Promise封装HTTP请求
  • 支持请求/响应拦截器
  • 请求取消机制
  • 统一错误处理

步骤2: 使用HTTP服务

typescript 复制代码
// 在页面中使用HTTP服务

import { createHttpService } from '../utils/HttpService';

const httpService = createHttpService('https://api.example.com');

@Entry
@Component
struct ApiTestPage {
  @State data: any[] = [];
  @State isLoading: boolean = false;
  
  async onPageShow() {
    await this.loadData();
  }
  
  async loadData() {
    this.isLoading = true;
    
    try {
      const response = await httpService.get('/articles', { page: 1, limit: 10 });
      
      if (response.success) {
        this.data = response.data;
      }
    } catch (error) {
      console.error('请求失败:', error);
      prompt.showToast({ message: error.message || '请求失败' });
    } finally {
      this.isLoading = false;
    }
  }
  
  async submitForm(formData: Record<string, any>) {
    try {
      const response = await httpService.post('/articles', formData);
      
      if (response.success) {
        prompt.showToast({ message: '提交成功' });
      }
    } catch (error) {
      prompt.showToast({ message: error.message || '提交失败' });
    }
  }
  
  build() {
    // 页面内容
  }
}

设计要点:

  • 创建HTTP服务实例
  • 使用get/post方法发送请求
  • 处理成功和失败情况

步骤3: 请求重试机制

typescript 复制代码
// 请求重试包装器

export class RetryHttpService extends HttpService {
  private retryDelay: number = 1000; // 重试间隔(毫秒)
  
  async requestWithRetry<T>(
    method: HttpMethod,
    url: string,
    data?: Record<string, any>,
    params?: Record<string, any>,
    retryCount: number = 0
  ): Promise<HttpResponse<T>> {
    try {
      return await this.request<T>(method, url, data, params);
    } catch (error) {
      // 如果是网络错误,进行重试
      if (error.code === 'NETWORK_ERROR' && retryCount < this.maxRetries) {
        // 等待一段时间后重试
        await this.delay(this.retryDelay * Math.pow(2, retryCount));
        return this.requestWithRetry(method, url, data, params, retryCount + 1);
      }
      
      throw error;
    }
  }
  
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  // 重写父类方法
  async get<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
    return this.requestWithRetry<T>('GET', url, undefined, params);
  }
  
  async post<T>(url: string, data?: Record<string, any>): Promise<HttpResponse<T>> {
    return this.requestWithRetry<T>('POST', url, data);
  }
}

设计要点:

  • 指数退避策略
  • 网络错误自动重试
  • 可配置最大重试次数

实际应用场景

场景1: API服务封装

在实际项目中,通常会为每个业务模块创建专门的API服务类:

typescript 复制代码
// api/ArticleApi.ets

import { createHttpService, HttpResponse } from '../utils/HttpService';

const httpService = createHttpService('https://api.jieqitong.com');

/**
 * 文章API服务
 */
export class ArticleApi {
  /**
   * 获取文章列表
   */
  static async getArticles(page: number = 1, limit: number = 10): Promise<HttpResponse<Article[]>> {
    return await httpService.get('/articles', { page, limit });
  }
  
  /**
   * 获取文章详情
   */
  static async getArticleDetail(id: string): Promise<HttpResponse<Article>> {
    return await httpService.get(`/articles/${id}`);
  }
  
  /**
   * 创建文章
   */
  static async createArticle(data: CreateArticleRequest): Promise<HttpResponse<Article>> {
    return await httpService.post('/articles', data);
  }
  
  /**
   * 更新文章
   */
  static async updateArticle(id: string, data: UpdateArticleRequest): Promise<HttpResponse<Article>> {
    return await httpService.put(`/articles/${id}`, data);
  }
  
  /**
   * 删除文章
   */
  static async deleteArticle(id: string): Promise<HttpResponse<void>> {
    return await httpService.delete(`/articles/${id}`);
  }
}

interface Article {
  id: string;
  title: string;
  content: string;
  author: string;
  readCount: number;
  createdAt: string;
}

interface CreateArticleRequest {
  title: string;
  content: string;
  category: string;
}

interface UpdateArticleRequest {
  title?: string;
  content?: string;
}

// 使用示例
async function loadArticles() {
  try {
    const response = await ArticleApi.getArticles(1, 20);
    if (response.success) {
      console.log('文章列表:', response.data);
    }
  } catch (error) {
    console.error('加载文章失败:', error);
  }
}

场景2: 文件上传

typescript 复制代码
// 文件上传封装
export class FileApi {
  static async uploadFile(filePath: string): Promise<HttpResponse<UploadResult>> {
    const formData = new FormData();
    formData.append('file', {
      uri: filePath,
      name: 'upload_file',
      type: 'image/jpeg'
    });
    
    const response = await fetch('https://api.jieqitong.com/upload', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${await getToken()}`
      },
      body: formData
    });
    
    return await response.json();
  }
}

interface UploadResult {
  url: string;
  fileName: string;
}

场景3: 批量请求处理

typescript 复制代码
// 批量请求封装
export class BatchRequest {
  /**
   * 并行请求多个接口
   */
  static async parallel<T extends any[]>(
    ...requests: Array<Promise<T[number]>>
  ): Promise<T> {
    return await Promise.all(requests) as T;
  }
  
  /**
   * 串行请求多个接口
   */
  static async serial<T>(
    requests: Array<() => Promise<T>>
  ): Promise<T[]> {
    const results: T[] = [];
    for (const request of requests) {
      const result = await request();
      results.push(result);
    }
    return results;
  }
}

// 使用示例
async function loadDashboardData() {
  // 并行加载多个数据
  const [articles, stats, notifications] = await BatchRequest.parallel(
    ArticleApi.getArticles(1, 5),
    StatsApi.getStats(),
    NotificationApi.getNotifications()
  );
  
  console.log('仪表盘数据加载完成');
}

常见问题与解决方案

问题1: 请求超时

现象: 请求长时间无响应

原因: 网络延迟、服务器响应慢

解决方案:

typescript 复制代码
// 设置合理的超时时间
const controller = new AbortController();
const timeoutId = setTimeout(() => {
  controller.abort();
}, 30000); // 30秒超时

try {
  const response = await fetch(url, {
    signal: controller.signal
  });
} finally {
  clearTimeout(timeoutId);
}

问题2: Token过期处理

现象: 接口返回401状态码

解决方案:

typescript 复制代码
// 在响应拦截器中统一处理
service.addResponseInterceptor(async (response) => {
  if (response.status === 401) {
    // 尝试刷新Token
    const refreshToken = await tokenManager.getRefreshToken();
    if (refreshToken) {
      try {
        const refreshResponse = await httpService.post('/refresh', { refreshToken });
        if (refreshResponse.success) {
          // 保存新Token
          await tokenManager.setToken(
            refreshResponse.data.accessToken,
            refreshResponse.data.refreshToken,
            refreshResponse.data.expireTime
          );
          // 重新发起原请求
          // 可以通过缓存请求配置实现自动重试
        }
      } catch {
        // 刷新失败,跳转到登录页
        await tokenManager.clearToken();
        router.pushUrl({ url: 'pages/Login' });
      }
    } else {
      // 没有refreshToken,直接跳转登录
      await tokenManager.clearToken();
      router.pushUrl({ url: 'pages/Login' });
    }
  }
});

问题3: 重复请求

现象: 快速点击按钮导致多次请求

解决方案:

typescript 复制代码
// 使用防抖或节流
function debounce<T extends (...args: any[]) => any>(
  func: T,
  wait: number
): (...args: Parameters<T>) => void {
  let timeout: number | null = null;
  return (...args: Parameters<T>) => {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

// 使用示例
const debouncedLoadData = debounce(async () => {
  await loadData();
}, 300);

Button('加载数据')
  .onClick(() => debouncedLoadData());

问题4: 跨域问题

现象: 请求被浏览器阻止,报CORS错误

解决方案:

typescript 复制代码
// 在服务端配置CORS
// 服务端响应头配置示例:
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
// Access-Control-Allow-Headers: Content-Type, Authorization

性能优化策略

策略1: 请求缓存

typescript 复制代码
// 实现请求缓存
class CachedHttpService extends HttpService {
  private cache: Map<string, CachedResponse> = new Map();
  private cacheTtl: number = 5 * 60 * 1000; // 5分钟缓存
  
  async get<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
    const cacheKey = this.buildCacheKey(url, params);
    
    // 检查缓存
    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() < cached.expireTime) {
      return cached.response;
    }
    
    // 发起请求
    const response = await super.get<T>(url, params);
    
    // 缓存成功的GET请求
    if (response.success) {
      this.cache.set(cacheKey, {
        response,
        expireTime: Date.now() + this.cacheTtl
      });
    }
    
    return response;
  }
  
  private buildCacheKey(url: string, params?: Record<string, any>): string {
    const queryString = params ? JSON.stringify(params) : '';
    return `${url}_${queryString}`;
  }
  
  clearCache(): void {
    this.cache.clear();
  }
}

interface CachedResponse {
  response: HttpResponse<any>;
  expireTime: number;
}

策略2: 请求合并

typescript 复制代码
// 批量请求合并
class BatchHttpService extends HttpService {
  private pendingRequests: Map<string, Array<(response: any) => void>> = new Map();
  
  async getWithBatch<T>(url: string, params?: Record<string, any>): Promise<HttpResponse<T>> {
    const key = this.buildRequestKey(url, params);
    
    return new Promise((resolve) => {
      // 如果有相同请求正在进行,加入等待队列
      if (this.pendingRequests.has(key)) {
        this.pendingRequests.get(key)!.push(resolve);
        return;
      }
      
      // 标记请求正在进行
      this.pendingRequests.set(key, [resolve]);
      
      // 执行请求
      super.get<T>(url, params).then((response) => {
        // 通知所有等待的请求
        const resolvers = this.pendingRequests.get(key)!;
        resolvers.forEach(r => r(response));
        
        // 清理状态
        this.pendingRequests.delete(key);
      });
    });
  }
  
  private buildRequestKey(url: string, params?: Record<string, any>): string {
    return `${url}_${JSON.stringify(params)}`;
  }
}

本章小结

核心知识点

本文完成了HTTP请求封装的深度解析:

1. HTTP协议原理

  • 请求/响应结构
  • 状态码分类
  • 常用状态码详解

2. 拦截器机制

  • 请求拦截器(Token注入、参数处理)
  • 响应拦截器(错误处理、数据转换)

3. 请求取消机制

  • AbortController实现
  • 应用场景(防抖、页面切换)

4. 请求重试机制

  • 指数退避策略
  • 重试条件判断

5. 实际应用场景

  • API服务封装
  • 文件上传
  • 批量请求处理

6. 常见问题解决方案

  • 请求超时
  • Token过期
  • 重复请求
  • 跨域问题

7. 性能优化策略

  • 请求缓存
  • 请求合并

下一步预告

HTTP请求封装已经完成!在下一篇文章中,我们将学习:

  • 全局数据通信
  • 全局状态管理
  • 事件总线
  • 数据共享

节气通应用已发布上线,可在应用市场下载体验


相关链接

相关推荐
yuegu7771 小时前
HarmonyOS应用<节气通>开发第22篇:HolidayCard组件封装
华为·harmonyos
芒鸽1 小时前
HarmonyOS ArkUI 组件开发实战:自定义组件与高级布局详解
华为·harmonyos
IT大白鼠2 小时前
BGP多归属技术原理与应用实践
网络·网络协议·华为
祭曦念2 小时前
鸿蒙Next实战-笑话大全App开发
华为·harmonyos
三声三视2 小时前
Electron 鸿蒙快捷键全失灵,我排查了六个小时
华为·electron·harmonyos·鸿蒙
风华圆舞2 小时前
鸿蒙构建失败时,先查 Flutter 还是先查 Hvigor
flutter·华为·harmonyos
YM52e2 小时前
鸿蒙HarmonyOS ArkTS 实战:教师座椅出入记录 APP 从零到一
学习·华为·harmonyos·鸿蒙系统
狼哥16862 小时前
蛋糕美食元服务_订单实现指南
ui·harmonyos
忧云2 小时前
HTTP抓包工具:安装配置与使用教程
网络协议·网络抓包工具·http抓包