HarmonyOS网络请求框架的简单实现

42 阅读4分钟

HarmonyOS网络请求框架的简单实现

HarmonyOS中实现一个简单的网络请求框架(如类似于 Android 的 Retrofit、OkHttp),可以基于其 JS/TS API(如fetch、httpRequest等)来封装。以TypeScript为例,给出一个简易的网络请求框架实现(AskTs风格,易于扩展和二次封装)。

1. 概述

HttpClient 是一个基于 HarmonyOS 的单例 HTTP 客户端实现,提供了类型安全的 HTTP 请求功能。它支持多种 HTTP 方法,并包含内置的错误处理和响应解析功能。

2. 特性

- 单例模式实现
- 支持多种 HTTP 方法(GET、POST、PUT、DELETE、PATCH)
- 自动请求头管理
- 内置错误处理机制
- 类型安全的响应解析
- 支持查询参数和请求体
- 可配置的超时时间
- JSON 请求/响应处理

3. 发送请求

3.1 GET 请求

// 基础 GET 请求
const response = await httpClient.get<ResponseType>('https://api.example.com/data');

// 带查询参数的 GET 请求
const response = await httpClient.get<ResponseType>(
  'https://api.example.com/data',
  { param1: 'value1', param2: 'value2' }
);

// 带自定义请求头的 GET 请求
const response = await httpClient.get<ResponseType>(
  'https://api.example.com/data',
  { param1: 'value1' },
  { 'Custom-Header': 'value' }
);

3.2 POST 请求

// 基础 POST 请求
const response = await httpClient.post<ResponseType>(
  'https://api.example.com/data',
  { key: 'value' }
);

// 带自定义请求头的 POST 请求
const response = await httpClient.post<ResponseType>(
  'https://api.example.com/data',
  { key: 'value' },
  { 'Custom-Header': 'value' }
);

3.3 PUT 请求

const response = await httpClient.put<ResponseType>(
  'https://api.example.com/data',
  { key: 'value' }
);

3.4 DELETE 请求

const response = await httpClient.delete<ResponseType>('https://api.example.com/data');

3.5 PATCH 请求

const response = await httpClient.patch<ResponseType>(
  'https://api.example.com/data',
  { key: 'value' }
);

4. 错误处理

客户端包含内置的错误处理机制,针对常见的 HTTP 状态码提供特定的错误信息:

- 404: "请求的资源不存在"
- 500: "服务器错误,请稍后再试"
- 502: "网关错误"
- 503: "服务不可用"
- 504: "网关超时"
- 其他错误: "网络错误,请检查连接"

5. 响应格式

客户端期望的响应格式如下:

interface BaseResponse<T> {
  code: number;      // 业务状态码
  message: string;   // 响应消息
  biz?: T;          // 业务数据
}

如果响应中包含 biz 字段,将自动提取并返回该字段的内容。否则,将返回整个响应内容。

6. 请求头

6.1 默认请求头:在所有请求中包含以下请求头:

const defaultHeaders: Record<string, string> = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

6.2 自定义的请求头

// 合并请求头
private mergeHeaders(customHeaders?: Record<string, string>): Record<string, string> {
  const mergedHeaders: Record<string, string> = {};
  // 先复制 defaultHeaders
  Object.keys(defaultHeaders).forEach((key) => {
    mergedHeaders[key] = defaultHeaders[key];
  })
  // 如果存在 customHeaders,再覆盖
  if (customHeaders) {
    Object.keys(customHeaders).forEach((key) => {
      mergedHeaders[key] = customHeaders[key];
    });
  }
  return mergedHeaders;
}

7. 完整的示例

import 'reflect-metadata';
import http from '@ohos.net.http'
import logger from './logger';
import { HttpError, BaseResponse } from './ResponseData';
import { RequestConfig, HttpRequestBody } from './RequestData';

/**
 * @classdesc 网络请求客户端
 * @author zw
 * @date 2025/4/14
 */

export class HttpClient {
  private httpRequest: http.HttpRequest
  private static instance: HttpClient;

  constructor() {
    this.httpRequest = http.createHttp()
  }

  public static getInstance(): HttpClient {
    if (!HttpClient.instance) {
      HttpClient.instance = new HttpClient();
    }
    return HttpClient.instance;
  }

  // 发送请求
  private async request<T>(config: RequestConfig): Promise<T> {
    try {
      const options: http.HttpRequestOptions = {
        method: config.method as http.RequestMethod,
        header: this.mergeHeaders(config.headers),
        readTimeout: 60000,
        connectTimeout: 60000
      }

      // 处理GET参数
      let finalUrl = config.url
      if (config.method == 'GET') {
        if (config.params && Object.keys(config.params).length > 0) {
          finalUrl += `?${this.serializeParams(config.params)}`
        }
      } else {
        // 处理POST/PUT数据
        if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method!) && config.body) {
          options.extraData = JSON.stringify(config.body)
        }
      }


      const response = await this.httpRequest.request(finalUrl, options)

      // 检查响应状态
      if (response.responseCode == 200) {
        // 解析响应数据
        try {
          let baseResponse: BaseResponse<T> = {} as BaseResponse<T>
          let result: object = JSON.parse(response.result.toString())
          const hasBiz: boolean = this.checkKeyInObject(result, 'biz')
          console.log('result', JSON.stringify(result))
          if (hasBiz) {
            baseResponse = JSON.parse(response.result.toString())
            // 检查业务状态码
            if (baseResponse.code !== 0) {
              throw new HttpError(baseResponse.code, baseResponse.message)
            }
            return baseResponse.biz as T
          } else {
            return response.result as T
          }
        } catch (e) {
          throw new HttpError(response.responseCode, response.result.toString())
        }

      } else {
        throw new HttpError(response.responseCode, response.result.toString())
      }


    } catch (error) {
      logger.error(new Error(`Unknown HTTP Error: ${JSON.stringify(error)}`))
      // 根据错误码显示不同的提示信息
      if (error instanceof HttpError) {
        let errorMessage = error.message
        switch (error.code) {
          case 404:
            errorMessage = '请求的资源不存在'
            break
          case 500:
            errorMessage = '服务器错误,请稍后再试'
            break
          case 502:
            errorMessage = '网关错误'
            break
          case 503:
            errorMessage = '服务不可用'
            break
          case 504:
            errorMessage = '网关超时'
            break
          default:
            errorMessage = '网络错误,请检查连接'
            break
        }
        throw new HttpError(error.code, errorMessage)
      } else {
        throw new HttpError(error.code, '网络错误,请检查连接')
      }
    }
  }

  // 合并请求头
  private mergeHeaders(customHeaders?: Record<string, string>): Record<string, string> {
    const mergedHeaders: Record<string, string> = {};
    // 先复制 defaultHeaders
    Object.keys(defaultHeaders).forEach((key) => {
      mergedHeaders[key] = defaultHeaders[key];
    })
    // 如果存在 customHeaders,再覆盖,这里可以自定义请求
    if (customHeaders) {
      Object.keys(customHeaders).forEach((key) => {
        mergedHeaders[key] = customHeaders[key];
      });
    }
    return mergedHeaders;
  }

  // 序列化参数
  private serializeParams(params: Record<string, string | number | boolean>): string {
    return Object.keys(params)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
      .join('&')
  }

  // GET请求
  public async get<T>(url: string, params?: Record<string, string | number>,
    headers?: Record<string, string>): Promise<T> {
    return this.request<T>({
      url,
      method: 'GET',
      params,
      headers
    })
  }

  // POST请求
  public async post<T>(url: string, body?: object, headers?: Record<string, string>): Promise<T> {
    return this.request<T>({
      url,
      method: 'POST',
      body,
      headers
    })
  }

  // PUT请求
  public async put<T,>(url: string, body?: object, headers?: Record<string, string>): Promise<T> {
    return this.request<T>({
      url,
      method: 'PUT',
      body,
      headers
    })
  }

  // DELETE请求
  public async delete<T>(url: string, headers?: Record<string, string>): Promise<T> {
    return this.request<T>({
      url,
      method: 'DELETE',
      headers
    })
  }

  // PATCH请求
  public async patch<T>(url: string, body?: object, headers?: Record<string, string>): Promise<T> {
    return this.request<T>({
      url,
      method: 'PATCH',
      body,
      headers
    })
  }

  // 递归检查对象中是否包含指定的key
  private checkKeyInObject(obj: object, targetKey: string): boolean {
    // 如果是数组,遍历每个元素
    if (Array.isArray(obj)) {
      for (let i = 0; i < obj.length; i++) {
        const item: object | string | number | boolean | null = obj[i] as object | string | number | boolean | null
        if (item !== null && typeof item === 'object') {
          if (this.checkKeyInObject(item, targetKey)) {
            return true
          }
        }
      }
      return false
    }

    // 使用Object.keys()遍历对象
    const keys: string[] = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      const key: string = keys[i]
      if (key === targetKey) {
        return true
      }
      const value: object | string | number | boolean | null = obj[key] as object | string | number | boolean | null
      if (value !== null && typeof value === 'object') {
        if (this.checkKeyInObject(value, targetKey)) {
          return true
        }
      }
    }
    return false
  }
}

const defaultHeaders: Record<string, string> = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}
import { HttpClient } from "../HttpClient"
import { UserApi } from "../api/UserApi"

export class WeatherRequest {
  static async getWeather(params?: Record<string, string | number>): Promise<String> {
    return await HttpClient.getInstance().get<string>(UserApi.indexApiUrl, params);
  }
}
export class UserApi {
  static indexApiUrl: string = 'http://v.juhe.cn/weather/index';
}
import { WeatherRequest } from '../http/request/WeatherRequest';
import { UserInfo } from '../http/UserInfo';

@Entry
@Component
struct NewDetailsPage {
  @State message: string = '获取详情中。。。';
  appKey: string = '51bbd89bb89e50b1668534882e7baeb0';

  build() {
    Column() {
      Button('获取数据')
        .id('NewDetailsPageHelloWorld')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width(200)
        .height(40)
        .margin({ top: 20 })
        .onClick(() => {
          this.getWeather();
        })

      Scroll() {
        Column() {
          Text(this.message)
            .fontSize(20)
            .textAlign(TextAlign.Start)
            .width('100%')
        }
        .width('100%')
        .padding(10)
        .justifyContent(FlexAlign.Start) // 确保内容从顶部开始
      }
      .height('60%')
      .width('100%')
      .margin({ top: 10 }) // 确保 Scroll 顶部没有 margin
      .scrollable(ScrollDirection.Vertical)
    }
    .height('100%')
    .width('100%')
  }

  public async getWeather() {
    let map: Record<string, string> = {
      "key": this.appKey,
      "cityname": "合肥",
      "dtype": "json",
      "format": ""
    }

    const resp = await WeatherRequest.getWeather(map);
    let userInfo: UserInfo = JSON.parse(resp.toString()) as UserInfo
    this.message = resp.toString()
    console.log("NewDetailsPage:==" + JSON.stringify(resp))
  }
}