💭

HarmonyOS実践開発:ネットワーク層のアート——エレガントなカプセル化と構築ガイド(中編)

に公開

まえがき

HarmonyOSの広大な開発の世界で、ネットワーク層の構築とカプセル化は、効率的で安定したアプリケーションを構築するための礎石です。前回の探求に続き、本稿ではネットワーク層の最適化の旅を深く掘り下げ、タイプコンバーター、リクエストクエリアッパダー、豊富な定数パラメーターを通じて、ネットワーク層の構築の芸術を新たな高みへと引き上げる方法を解明します。

一、ネットワークリクエストの深度最適化

データ型コンバーター:定義と実践

ネットワークリクエストの世界では、データ形式の変換が極めて重要です。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 クライアントエラー
  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 サーバーエラー
  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などのクラスを定義して、エラーの種類が一目でわかるようにし、開発者が問題を迅速に特定できるようにしました。

// カスタムエラータイプ
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
  }
}

// ネットワーク要求コードエラー
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クラスを使用して、強力で柔軟なネットワーク要求処理メカニズムを実現する方法を示します。このクラスは、データ変換、パラメーター附加、例外処理などのすべてのコア機能を統合しています。

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. RequestOptions.ets 設定クラスの作成
export interface RequestOptions {
  baseUrl?: string;
  act?: string;
  method?: RequestMethod; // デフォルトは 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);
  }

}

結語

本稿では、データ変換からエラー処理まで、ネットワーク層のカプセル化と最適化を深く掘り下げ、効率的なネットワークサービスの構築のアートを体現しました。これらの実践が、開発者がHarmonyOSの開発で自在に動くのを助けて、より強健でユーザーフriendlyなアプリケーションを構築することを願っています。

Discussion