鸿蒙实战开发:网络层的艺术——优雅封装与搭建指南(中)

前言

在鸿蒙开发的广袤天地中,网络层的搭建与封装无疑是构建高效、稳定应用的基石。继上篇的探索之后,本文将继续深入网络层的优化之旅,揭秘如何通过类型转换器、请求查询附加器以及丰富的常量参数,将网络层的构建艺术推向一个新的高度。

一、网络请求的深度优化

数据类型转换器:定义与实践

在网络请求的世界里,数据格式的转换至关重要。我们通过定义DataConverter接口,实现了对请求与响应数据类型的灵活转换。

复制代码
export interface DataConverter {
  requestConvert(extraData: string | Object | ArrayBuffer): string | Object | ArrayBuffer;
  responseConvert(data: string | Object | ArrayBuffer, responseType: http.HttpDataType): string | Object | ArrayBuffer;
}
默认数据转换器:JSON转换器的实现

我们实现了一个默认的JsonDataConverter,它将请求数据转换为JSON字符串,并根据响应类型将响应数据转换为适当的格式。

复制代码
export class JsonDataConverter implements DataConverter {
  requestConvert(extraData: string | Object | ArrayBuffer): string | Object | ArrayBuffer {
    // 将请求数据转换为JSON字符串
    return JSONUtil.beanToJsonStr(extraData);
  }

  responseConvert(data: string | Object | ArrayBuffer, responseType: http.HttpDataType): string | Object | ArrayBuffer {
    // 根据responseType将响应数据转换为相应的格式
    switch (responseType) {
      case http.HttpDataType.STRING:
        return JSON.parse(data as string);
      case http.HttpDataType.OBJECT:
        return data;
      default:
        return data;
    }
  }
}

参数附加器:灵活重组请求数据

参数附加器QueryParamAppender接口允许我们对发送的请求数据进行重组,满足诸如参数签名等业务需求。

复制代码
// 定义一个用于附加查询参数的接口
export interface QueryParamAppender {
  append(queryParams?: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >): string|undefined;
}
默认附加器:简化查询参数的处理

通过CustomQueryParamAppender的实现,我们简化了查询参数的编码和附加过程。

复制代码
export class CustomQueryParamAppender implements QueryParamAppender {
  append(queryParams?: Map<string, string | number | boolean | number[] | string[] | boolean[]> | undefined): string|undefined {
    if (queryParams===undefined || queryParams.size === 0) {
      return;
    }
    const paramsArray: string[] = [];
    for (const qp of queryParams) {
      let key = qp[0]
      let value = qp[1]
      let encodedValue = '';
      if (Array.isArray(value)) {
        for (let i = 0; i < value.length; i++) {
          encodedValue += `${encodeURIComponent(`${key}[${i}]`)}=${encodeURIComponent(value[i].toString())}&`;
        }
        if (encodedValue.length > 0) {
          encodedValue = encodedValue.slice(0, -1); // 移除最后一个 '&'
        }
      } else {
        encodedValue = encodeURIComponent(key) + '=' + encodeURIComponent(value.toString());
      }
      paramsArray.push(encodedValue);
    }
    return paramsArray.join('&');
  }

}

二、常量定义:构建网络层的坚实基础

通过定义一系列的常量,我们为网络请求的错误处理提供了统一的接口。这些常量不仅包括了各种网络错误的场景,还涵盖了HTTP状态码的含义,为开发者提供了清晰的指导。

复制代码
{
      "name": "network_unavailable",
      "value": "网络不可用"
    },
    {
      "name": "invalid_url_format",
      "value": "URL格式不合法"
    },
    {
      "name": "invalid_url_not_exist",
      "value": "URL不存在"
    },
    {
      "name": "parameter_error",
      "value": "参数错误"
    },
    {
      "name": "permission_denied",
      "value": "权限被拒绝"
    },
    {
      "name": "unsupported_protocol",
      "value": "不支持的协议"
    },
    {
      "name": "bad_url_format",
      "value": "URL使用错误的/非法的格式或缺少URL"
    },
    {
      "name": "could_not_resolve_proxy_name",
      "value": "无法解析代理名称"
    },
    {
      "name": "could_not_resolve_host_name",
      "value": "无法解析主机名"
    },
    {
      "name": "could_not_connect_to_server",
      "value": "无法连接到服务器"
    },
    {
      "name": "weird_server_reply",
      "value": "服务器回复异常"
    },
    {
      "name": "access_denied_to_remote_resource",
      "value": "访问远程资源被拒绝"
    },
    {
      "name": "http2_framing_layer_error",
      "value": "HTTP2帧层错误"
    },
    {
      "name": "transferred_partial_file",
      "value": "传输了部分文件"
    },
    {
      "name": "failed_writing_data_to_disk",
      "value": "将数据写入磁盘/应用程序失败"
    },
    {
      "name": "upload_failed",
      "value": "上传失败"
    },
    {
      "name": "failed_to_open_read_local_data",
      "value": "无法打开/读取本地数据"
    },
    {
      "name": "out_of_memory",
      "value": "内存不足"
    },
    {
      "name": "timeout_reached",
      "value": "达到超时时间"
    },
    {
      "name": "redirects_exceeded",
      "value": "达到重定向的最大次数"
    },
    {
      "name": "server_returned_nothing",
      "value": "服务器未返回任何内容(无头信息,无数据)"
    },
    {
      "name": "failed_sending_data_to_peer",
      "value": "向对等端发送数据失败"
    },
    {
      "name": "failure_receiving_data_from_peer",
      "value": "从对等端接收数据失败"
    },
    {
      "name": "ssl_certificate_problem",
      "value": "本地SSL证书问题"
    },
    {
      "name": "unsupported_ssl_cipher",
      "value": "不支持指定的SSL加密算法"
    },
    {
      "name": "ssl_peer_certificate_or_ssh_remote_key_not_ok",
      "value": "SSL对等证书或SSH远程密钥不正确"
    },
    {
      "name": "unrecognized_http_content_or_transfer_encoding",
      "value": "无法识别的HTTP内容或传输编码"
    },
    {
      "name": "maximum_file_size_exceeded",
      "value": "超过最大文件大小"
    },
    {
      "name": "disk_full_or_allocation_exceeded",
      "value": "磁盘已满或分配超过限制"
    },
    {
      "name": "remote_file_already_exists",
      "value": "远程文件已存在"
    },
    {
      "name": "ssl_ca_cert_problem",
      "value": "SSL CA证书问题(路径?访问权限?)"
    },
    {
      "name": "remote_file_not_found",
      "value": "远程文件未找到"
    },
    {
      "name": "authentication_function_error",
      "value": "身份验证函数返回错误"
    },
    {
      "name": "unknown_other_error",
      "value": "未知的其他错误"
    },
    {
      "name": "bad_request",
      "value": "客户端请求的语法错误,服务器无法理解。"
    },
    {
      "name": "unauthorized",
      "value": "请求要求身份验证。"
    },
    {
      "name": "forbidden",
      "value": "服务器理解请求客户端的请求,但是拒绝执行此请求。"
    },
    {
      "name": "not_found",
      "value": "服务器无法根据客户端的请求找到资源(网页)。"
    },
    {
      "name": "method_not_allowed",
      "value": "客户端请求中的方法被禁止。"
    },
    {
      "name": "request_timeout",
      "value": "请求超时。"
    },
    {
      "name": "unsupported_media_type",
      "value": "服务器不支持请求的格式(如请求中包含了服务器不支持的MIME类型)。"
    },
    {
      "name": "internal_server_error",
      "value": "服务器内部错误,无法完成请求。"
    },
    {
      "name": "bad_gateway",
      "value": "作为网关或代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。"
    },
    {
      "name": "service_unavailable",
      "value": "由于超载或系统维护,服务器目前无法处理请求。"
    },
    {
      "name": "gateway_timeout",
      "value": "作为网关或代理工作的服务器尝试执行请求时,未能及时从上游服务器收到需要的响应。"
    }

常量类代码使用

复制代码
import { Application } from '../../app/Application'
import { NetworkError } from '../../exception/NetworkError'

export class NetworkServiceErrorConst {
  // 网络不可用
  static readonly UN_AVILABLE: number = 100000
  // url错误
  static readonly URL_ERROR: number = 100001
  // url 不存在 错误
  static readonly URL_NOT_EXIST_ERROR: number = 100002

  static readonly PARAMETER_ERROR: number = 401;
  static readonly PERMISSION_DENIED: number = 201;
  static readonly UNSUPPORTED_PROTOCOL: number = 2300001;
  static readonly BAD_URL_FORMAT: number = 2300003;
  static readonly COULD_NOT_RESOLVE_PROXY_NAME: number = 2300005;
  static readonly COULD_NOT_RESOLVE_HOST_NAME: number = 2300006;
  static readonly COULD_NOT_CONNECT_TO_SERVER: number = 2300007;
  static readonly WEIRD_SERVER_REPLY: number = 2300008;
  static readonly ACCESS_DENIED_TO_REMOTE_RESOURCE: number = 2300009;
  static readonly HTTP2_FRAMING_LAYER_ERROR: number = 2300016;
  static readonly TRANSFERRED_PARTIAL_FILE: number = 2300018;
  static readonly FAILED_WRITING_DATA_TO_DISK: number = 2300023;
  static readonly UPLOAD_FAILED: number = 2300025;
  static readonly FAILED_TO_OPEN_READ_LOCAL_DATA: number = 2300026;
  static readonly OUT_OF_MEMORY: number = 2300027;
  static readonly TIMEOUT_REACHED: number = 2300028;
  static readonly REDIRECTS_EXCEEDED: number = 2300047;
  static readonly SERVER_RETURNED_NOTHING: number = 2300052;
  static readonly FAILED_SENDING_DATA_TO_PEER: number = 2300055;
  static readonly FAILURE_RECEIVING_DATA_FROM_PEER: number = 2300056;
  static readonly SSL_CERTIFICATE_PROBLEM: number = 2300058;
  static readonly UNSUPPORTED_SSL_CIPHER: number = 2300059;
  static readonly SSL_PEER_CERTIFICATE_OR_SSH_REMOTE_KEY_NOT_OK: number = 2300060;
  static readonly UNRECOGNIZED_HTTP_CONTENT_OR_TRANSFER_ENCODING: number = 2300061;
  static readonly MAXIMUM_FILE_SIZE_EXCEEDED: number = 2300063;
  static readonly DISK_FULL_OR_ALLOCATION_EXCEEDED: number = 2300070;
  static readonly REMOTE_FILE_ALREADY_EXISTS: number = 2300073;
  static readonly SSL_CA_CERT_PROBLEM: number = 2300077;
  static readonly REMOTE_FILE_NOT_FOUND: number = 2300078;
  static readonly AUTHENTICATION_FUNCTION_ERROR: number = 2300094;
  static readonly UNKNOWN_OTHER_ERROR: number = 2300999;
  // 4xx Client Error
  static readonly BAD_REQUEST: number = 400;
  static readonly UNAUTHORIZED: number = 401;
  static readonly FORBIDDEN: number = 403;
  static readonly NOT_FOUND: number = 404;
  static readonly METHOD_NOT_ALLOWED: number = 405;
  static readonly REQUEST_TIMEOUT: number = 408;
  static readonly UNSUPPORTED_MEDIA_TYPE: number = 415;

  // 5xx Server Error
  static readonly INTERNAL_SERVER_ERROR: number = 500;
  static readonly BAD_GATEWAY: number = 502;
  static readonly SERVICE_UNAVAILABLE: number = 503;
  static readonly GATEWAY_TIMEOUT: number = 504;

  public static getNetworkError(code: number): NetworkError{
    return new NetworkError(code, NetworkServiceErrorConst.getErrorReason(code));
  }

  public static getErrorReason(errorCode: number): string {
    let reason = "";
    switch (errorCode) {
      case NetworkServiceErrorConst.UN_AVILABLE:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.network_unavailable'));
        break;
      case NetworkServiceErrorConst.URL_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.invalid_url_format'));
        break;
      case NetworkServiceErrorConst.URL_NOT_EXIST_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.invalid_url_not_exist'));
        break;
      case NetworkServiceErrorConst.PARAMETER_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.parameter_error'));
        break;
      case NetworkServiceErrorConst.PERMISSION_DENIED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.permission_denied'));
        break;
      case NetworkServiceErrorConst.UNSUPPORTED_PROTOCOL:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unsupported_protocol'));
        break;
      case NetworkServiceErrorConst.BAD_URL_FORMAT:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.bad_url_format'));
        break;
      case NetworkServiceErrorConst.COULD_NOT_RESOLVE_PROXY_NAME:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.could_not_resolve_proxy_name'));
        break;
      case NetworkServiceErrorConst.COULD_NOT_RESOLVE_HOST_NAME:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.could_not_resolve_host_name'));
        break;
      case NetworkServiceErrorConst.COULD_NOT_CONNECT_TO_SERVER:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.could_not_connect_to_server'));
        break;
      case NetworkServiceErrorConst.WEIRD_SERVER_REPLY:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.weird_server_reply'));
        break;
      case NetworkServiceErrorConst.ACCESS_DENIED_TO_REMOTE_RESOURCE:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.access_denied_to_remote_resource'));
        break;
      case NetworkServiceErrorConst.HTTP2_FRAMING_LAYER_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.http2_framing_layer_error'));
        break;
      case NetworkServiceErrorConst.TRANSFERRED_PARTIAL_FILE:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.transferred_partial_file'));
        break;
      case NetworkServiceErrorConst.FAILED_WRITING_DATA_TO_DISK:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.failed_writing_data_to_disk'));
        break;
      case NetworkServiceErrorConst.UPLOAD_FAILED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.upload_failed'));
        break;
      case NetworkServiceErrorConst.FAILED_TO_OPEN_READ_LOCAL_DATA:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.failed_to_open_read_local_data'));
        break;
      case NetworkServiceErrorConst.OUT_OF_MEMORY:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.out_of_memory'));
        break;
      case NetworkServiceErrorConst.TIMEOUT_REACHED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.timeout_reached'));
        break;
      case NetworkServiceErrorConst.REDIRECTS_EXCEEDED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.redirects_exceeded'));
        break;
      case NetworkServiceErrorConst.SERVER_RETURNED_NOTHING:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.server_returned_nothing'));
        break;
      case NetworkServiceErrorConst.FAILED_SENDING_DATA_TO_PEER:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.failed_sending_data_to_peer'));
        break;
      case NetworkServiceErrorConst.FAILURE_RECEIVING_DATA_FROM_PEER:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.failure_receiving_data_from_peer'));
        break;
      case NetworkServiceErrorConst.SSL_CERTIFICATE_PROBLEM:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.ssl_certificate_problem'));
        break;
      case NetworkServiceErrorConst.UNSUPPORTED_SSL_CIPHER:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unsupported_ssl_cipher'));
        break;
      case NetworkServiceErrorConst.SSL_PEER_CERTIFICATE_OR_SSH_REMOTE_KEY_NOT_OK:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.ssl_peer_certificate_or_ssh_remote_key_not_ok'));
        break;
      case NetworkServiceErrorConst.UNRECOGNIZED_HTTP_CONTENT_OR_TRANSFER_ENCODING:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unrecognized_http_content_or_transfer_encoding'));
        break;
      case NetworkServiceErrorConst.MAXIMUM_FILE_SIZE_EXCEEDED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.maximum_file_size_exceeded'));
        break;
      case NetworkServiceErrorConst.DISK_FULL_OR_ALLOCATION_EXCEEDED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.disk_full_or_allocation_exceeded'));
        break;
      case NetworkServiceErrorConst.REMOTE_FILE_ALREADY_EXISTS:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.remote_file_already_exists'));
        break;
      case NetworkServiceErrorConst.SSL_CA_CERT_PROBLEM:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.ssl_ca_cert_problem'));
        break;
      case NetworkServiceErrorConst.REMOTE_FILE_NOT_FOUND:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.remote_file_not_found'));
        break;
      case NetworkServiceErrorConst.AUTHENTICATION_FUNCTION_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.authentication_function_error'));
        break;
      case NetworkServiceErrorConst.UNKNOWN_OTHER_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unknown_other_error'));
        break;
      case NetworkServiceErrorConst.BAD_REQUEST:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.bad_request'));
        break;
      case NetworkServiceErrorConst.UNAUTHORIZED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unauthorized'));
        break;
      case NetworkServiceErrorConst.FORBIDDEN:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.forbidden'));
        break;
      case NetworkServiceErrorConst.NOT_FOUND:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.not_found'));
        break;
      case NetworkServiceErrorConst.METHOD_NOT_ALLOWED:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.method_not_allowed'));
        break;
      case NetworkServiceErrorConst.REQUEST_TIMEOUT:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.request_timeout'));
        break;
      case NetworkServiceErrorConst.UNSUPPORTED_MEDIA_TYPE:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unsupported_media_type'));
        break;
      case NetworkServiceErrorConst.INTERNAL_SERVER_ERROR:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.internal_server_error'));
        break;
      case NetworkServiceErrorConst.BAD_GATEWAY:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.bad_gateway'));
        break;
      case NetworkServiceErrorConst.SERVICE_UNAVAILABLE:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.service_unavailable'));
        break;
      case NetworkServiceErrorConst.GATEWAY_TIMEOUT:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.gateway_timeout'));
        break;
      default:
        reason = Application.getInstance().resourceManager.getStringSync($r('app.string.unknown_other_error'));
        break;
    }

    return reason;
  }

}

三、异常定义:清晰的错误处理策略

我们重新封装了网络请求错误,定义了BaseErrorNetworkError等类,使得错误类型一目了然,便于开发者快速定位问题。

typescript 复制代码
// 自定义错误类型
import { http } from '@kit.NetworkKit';

export abstract class BaseError extends Error{

}

//基本网络错误
export class NetworkError extends BaseError {

  code : number
  constructor(code: number,message: string) {
    super(message);
    this.name = 'NetworkError'
    this.code = code
  }
}

//网络请求code错误
export class NetworkResponseError extends BaseError {

  code : http.ResponseCode | number;
  constructor(code: http.ResponseCode | number,message: string) {
    super(message);
    this.name = 'NetworkResponseError'
    this.code = code
  }
}

四、拦截器:网络请求的守卫

通过优化拦截器接口,我们能够在请求发送前后以及发生错误时,执行特定的逻辑,如日志记录、权限验证等。

复制代码
export interface NetworkInterceptor {
  beforeRequest(request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void;
  afterResponse(response: http.HttpResponse , request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void;
  onError(error: BaseError, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void;
}

拦截器默认实现:

复制代码
import { NetworkInterceptor } from './NetworkInterceptor';
import { NetworkServiceErrorConst } from '../NetworkServiceErrorConst';
import { RequestOptions } from '../NetworkService';
import http from '@ohos.net.http';
import { LibLogManager } from '../../LibLog';
import { BaseError } from '../../../exception/NetworkError';
import { JSONUtil } from '../../JSONUtil';

const TAG = "DefaultInterceptor"

// 创建一个符合RequestOptions接口的对象
const requestOptions: RequestOptions = {
  baseUrl: 'https://api.example.com',
  act: 'someAction'
};

export class DefaultInterceptor implements NetworkInterceptor {

  beforeRequest(request: RequestOptions, httprequest: http.HttpRequest): void | Promise<void> {
    LibLogManager.getLogger().info(TAG,'request: ' + JSONUtil.beanToJsonStr(request));
    httprequest.on('headersReceive', (header) => {
      LibLogManager.getLogger().info(TAG,'header: ' + JSONUtil.beanToJsonStr(header));
    });
  }

  afterResponse(response: http.HttpResponse, request: RequestOptions, httprequest: http.HttpRequest): void | Promise<void> {
    httprequest.off('headersReceive');
    LibLogManager.getLogger().info(TAG,'response: ' + JSONUtil.beanToJsonStr(response));
  }

  onError(error: BaseError, request: RequestOptions, httprequest: http.HttpRequest): void | Promise<void> {
    httprequest.off('headersReceive');
    LibLogManager.getLogger().error(TAG,'error: ' + JSON.stringify(error));
  }

  

}

五、核型网络层代码:网络服务的心脏

在本节中,我们将展示如何通过NetworkService类,实现一个强大而灵活的网络请求处理机制。这个类集成了数据转换、参数附加、异常处理等所有核心功能。

typescript 复制代码
import { NetworkInterceptor } from './interceptor/NetworkInterceptor';
import { http } from '@kit.NetworkKit';
import { LibNetworkStatus } from '../network/LibNetworkStatus';
import { LibLogManager } from '../LibLog';
import { BaseError, NetworkError, NetworkResponseError } from '../../exception/NetworkError';
import { NetworkServiceErrorConst } from './NetworkServiceErrorConst';
import { Application } from '../../app/Application'
import { HashMap } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { DataConverter } from './converter/DataConverter';
import { QueryParamAppender } from './appender/QueryParamAppender';
import { CustomQueryParamAppender } from './appender/CustomQueryParamAppender';

// 1、创建RequestOption.ets 配置类
export interface RequestOptions {
  baseUrl?: string;
  act?: string;
  method?: RequestMethod; // default is GET
  queryParams ?: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >;
  header?: Record<string,string> | Map<string,string> | HashMap<string,string>;
  extraData?: string | Object | ArrayBuffer;
  expectDataType?: http.HttpDataType;
  usingCache?: boolean;
  priority?: number;
  connectTimeout?: number;
  readTimeout?: number;
  multiFormDataList?:Array<http.MultiFormData>;
}

export enum RequestMethod {
  OPTIONS = "OPTIONS",
  GET = "GET",
  HEAD = "HEAD",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE",
  TRACE = "TRACE",
  CONNECT = "CONNECT"
}

export class NetworkService {
  baseUrl:string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  private _dataConverter?: DataConverter | undefined; // 指定转换器

  public set dataConverter(value: DataConverter | undefined) {
    this._dataConverter = value;
  }

  private _queryParamAppender: QueryParamAppender = new CustomQueryParamAppender(); // 指定查询参数附加规则

  public set queryParamAppender(value: QueryParamAppender) {
    this._queryParamAppender = value;
  }

  private interceptors: NetworkInterceptor[] = [];

  addInterceptor(interceptor: NetworkInterceptor): void {
    this.interceptors.push(interceptor);
  }



  async request(requestOption: RequestOptions): Promise<http.HttpResponse> {
    let response: http.HttpResponse | null = null;
    let error: BaseError | null = null;
    // 每一个httpRequest对应一个HTTP请求任务,不可复用
    let httpRequest = http.createHttp();
    //开始发请求
    try {

      //如果url是传入的,则用传入的url
      requestOption.baseUrl = requestOption.baseUrl?requestOption.baseUrl:this.baseUrl;

      // 调用拦截器的beforeRequest方法
      for (const interceptor of this.interceptors) {
        await interceptor.beforeRequest(requestOption, httpRequest);
      }

      let url = requestOption.baseUrl + requestOption.act;

      if (this._queryParamAppender) {
        let param = this._queryParamAppender.append(requestOption.queryParams);
        if(param){
          url = url + "?" + param
        }
      }

      // 使用转换器转换请求数据
      if (this._dataConverter && requestOption.extraData) {
        requestOption.extraData = this._dataConverter.requestConvert(requestOption.extraData);
      }

      if(requestOption.baseUrl === null || requestOption.baseUrl.trim().length === 0){
        throw NetworkServiceErrorConst.getNetworkError(NetworkServiceErrorConst.URL_NOT_EXIST_ERROR)
      }

      if (!LibNetworkStatus.getInstance().isNetworkAvailable()) {
        LibLogManager.getLogger().error("HttpCore","网络不可用")
        throw NetworkServiceErrorConst.getNetworkError(NetworkServiceErrorConst.UN_AVILABLE)
      }

      if (!this.isValidUrl(requestOption.baseUrl)) {
        LibLogManager.getLogger().error("HttpCore","url格式不合法")
        throw NetworkServiceErrorConst.getNetworkError(NetworkServiceErrorConst.URL_ERROR)
      }

      let defalutHeader :Record<string,string> = {
        'Content-Type': 'application/json'
      }

      let expectDataType = requestOption.expectDataType||http.HttpDataType.STRING;
      response = await httpRequest.request(url , {
        method: requestOption.method,
        header: requestOption.header || defalutHeader,
        extraData: requestOption.extraData, // 当使用POST请求时此字段用于传递内容
        expectDataType: expectDataType, // 可选,指定返回数据的类型
        usingCache: requestOption.usingCache, // 可选,默认为true
        priority: requestOption.priority, // 可选,默认为1
        connectTimeout: requestOption.connectTimeout, // 可选,默认为60000ms
        readTimeout: requestOption.readTimeout, // 可选,默认为60000ms
        multiFormDataList: requestOption.multiFormDataList,
      })

      if (http.ResponseCode.OK !== response.responseCode) {
        throw new NetworkResponseError(response.responseCode, NetworkServiceErrorConst.getErrorReason(response.responseCode))
      }

      // 使用转换器转换响应数据
      if (response && this._dataConverter) {
        response.result = this._dataConverter.responseConvert(response.result, expectDataType);
      }

      // 调用拦截器的afterResponse方法
      for (const interceptor of this.interceptors) {
        await interceptor.afterResponse(response, requestOption, httpRequest);
      }

    } catch (e) {
      if(e instanceof NetworkResponseError || e instanceof NetworkError){
        error = e;
      } else {
        let err = e as BusinessError;
        error = NetworkServiceErrorConst.getNetworkError(err.code)
      }
    }

    // 根据是否有错误来调用拦截器的afterResponse或onError方法
    if (error) {
      for (const interceptor of this.interceptors) {
        await interceptor.onError(error, requestOption, httpRequest);
      }
      httpRequest.destroy();
      throw error; // 重新抛出错误以便调用者可以处理
    } else{
      httpRequest.destroy();
      return response!;
    }

  }

  private isValidUrl(url: string): boolean {
    // 正则表达式匹配 URL
    const urlPattern = new RegExp(
      '^(https?:\/\/)?' + // protocol
        '((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|' + // domain name
        '((\d{1,3}\.){3}\d{1,3}))' + // OR ip (v4) address
        '(\:\d+)?(\/[-a-z\d%_.~+]*)*' + // port and path
        '(\?[;&a-z\d%_.~+=-]*)?' + // query string
        '(\#[-a-z\d_]*)?$', // fragment locator
      'i' // ignore case
    );
    return urlPattern.test(url);
  }

}

结语

本文深入探讨了网络层的封装与优化,从数据转换到错误处理,每一步都体现了构建高效网络服务的艺术。希望这些实践能够帮助开发者在鸿蒙开发中游刃有余,构建出更加健壮和用户友好的应用。

相关推荐
一只栖枝4 小时前
华为 HCIE 大数据认证中 Linux 命令行的运用及价值
大数据·linux·运维·华为·华为认证·hcie·it
zhanshuo8 小时前
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
harmonyos
zhanshuo8 小时前
在鸿蒙中实现深色/浅色模式切换:从原理到可运行 Demo
harmonyos
whysqwhw13 小时前
鸿蒙分布式投屏
harmonyos
whysqwhw15 小时前
鸿蒙AVSession Kit
harmonyos
whysqwhw16 小时前
鸿蒙各种生命周期
harmonyos
whysqwhw17 小时前
鸿蒙音频编码
harmonyos
whysqwhw17 小时前
鸿蒙音频解码
harmonyos
whysqwhw17 小时前
鸿蒙视频解码
harmonyos
whysqwhw18 小时前
鸿蒙视频编码
harmonyos