🌊

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

に公開

HarmonyOSの広大な開発の世界で、ネットワーク層は情報交換の橋渡しとして、その重要性は言うまでもありません。本日は、皆さんと一緒に、芸術的な手法で、HarmonyOS公式のネットワークライブラリをエレガントにカプセル化し、アプリケーションに効率的で柔軟なネットワーク層を構築する方法を探求します。次回の章では、この完全にカプセル化されたネットワークライブラリをどのように使用して、ネットワーク層の開発と使用を簡単に驾驭するかについて詳しく説明します。

一、カプセル化の目的:拡張性とインターセプション性

HarmonyOSアプリケーションの開発では、ネットワークリクエストのカプセル化は、開発プロセスを簡素化するためだけでなく、コードの再利用可能性と保守性を向上させるためでもあります。私たちのカプセル化目標は、主に以下の2点を中心にしています。

  1. 拡張性:開発者がビジネスニーズに応じて、ネットワークリクエストの機能を簡単に拡張できるようにすること。例えば、カスタムリクエストヘッダーの追加、リクエストタイムアウトの設定など。
  2. インターセプション性:ネットワークリクエストのインターセプションメカニズムを提供し、リクエストを送信する前にまたはレスポンスが返された後に、ログ記録、エラー処理など、一連の操作を行うことができます。

二、基本要素の定義:エラーコンスタントと文字列

1. エラーコンスタントの定義

ネットワークリクエストにおけるエラーコードを統一管理するために、NetworkServiceErrorConstクラスを定義し、さまざまなネットワークリクエストで遭遇する可能性のあるエラーコードを格納します。

export class NetworkServiceErrorConst {
  // ネットワークが使用不可
  static readonly UN_AVAILABLE: number = 100000;
  // URLエラー
  static readonly URL_ERROR: number = 100001;
  // URLが存在しないエラー
  static readonly URL_NOT_EXIST_ERROR: number = 100002;
  // ネットワークエラー
  static readonly NET_ERROR: number = 100003;
  // ...その他の可能性のあるエラーコード
}

2. エラー文字列の定義

また、エラーコードに対応するエラー文字列を定義する必要があります。これは、アプリケーション内でユーザーに表示するためです。

{
"name": "network_unavailable",
"value": "ネットワークが使用できません"
},
{
"name": "invalid_url_format",
"value": "URL形式が不正です"
},
{
"name": "invalid_url_not_exist",
"value": "URLが存在しません"
}

三、実用ツールセット

URLの検証

ネットワークリクエストのURL形式が正しいことを確認するために、正規表現を使用してURLの有効性を検証するisValidUrl関数を提供します。

private isValidUrl(url: string): boolean {
    // 正規表現でさまざまな可能なURL形式にマッチ
    const urlPattern = new RegExp(
        '^(https?:\\/\\/)?' + // プロトコル
        '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // ドメイン名
        '((\\d{1,3}\\.){3}\\d{1,3}))' + // またはIPv4アドレス
        '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // ポートとパス
        '(\\?[;&a-z\\d%_.~+=-]*)?' + // クエリ文字列
        '(\\#[-a-z\\d_]*)?$', // フラグメント識別子
        'i' // 大小文字を区別しない
    );
    return urlPattern.test(url); // 検証結果を返す
}

// 使用例
if (isValidUrl("http://example.com")) {
    console.log("URLは有効です。");
} else {
    console.log("URLは無効です。");
}

パラメーターの連結

URLにクエリパラメーターを追加する必要がある場合、appendQueryParams関数が役立ちます。これは、単一の値や配列の値を持つパラメーターを処理し、自動的にエンコードを行います。

private appendQueryParams(url: string, queryParams: Map<string, any> | undefined): string {
    if (!queryParams || queryParams.size === 0) {
        return url;
    }

    const paramsArray: string[] = [];
    queryParams.forEach((value, key) => {
        if (Array.isArray(value)) {
            for (let i = 0; i < value.length; i++) {
                paramsArray.push(`${encodeURIComponent(`${key}[${i}]`)}=${encodeURIComponent(value[i].toString())}`);
            }
        } else {
            paramsArray.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
        }
    });

    // URLがすでにクエリパラメーターを含んでいるかどうかを確認し、'?'または'&'を使用するかどうかを決定する
    const separator = url.includes('?') ? '&' : '?';
    return url + separator + paramsArray.join('&');
}

// 使用例
const baseUrl = "http://example.com/search";
const params = new Map<string, any>();
params.set("q", "test");
params.set("page", 2);
const urlWithParams = appendQueryParams(baseUrl, params);
console.log(urlWithParams); // 出力: http://example.com/search?q=test&page=2

これらの2つのツール関数を使用することで、ネットワークリクエストのURLが正しいだけでなく、必要なクエリパラメーターが含まれていることを保証し、ネットワークリクエストの正確性と信頼性を向上させることができます。

四、ネットワークインターセプターの作成

ネットワークリクエストとレスポンスのプロセスで、ネットワークインターセプター(Interceptor)は非常に重要な概念です。リクエストを送信する前に、レスポンスを受信した後、またはエラーが発生した際に、特定のロジックを実行することができます。例えば、ネットワークパラメーターの追加、ログの記録、エラー処理などです。

まず、インターセプターが実装しなければならないメソッドを規定するNetworkInterceptorインターフェースを定義します。

import { http } from '@kit.NetworkKit'; 
import { RequestOptions } from '../NetworkService'; 

export interface NetworkInterceptor {
  beforeRequest(request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void;
  afterResponse(response: http.HttpResponse | object, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void;
  onError(error: Error, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void;
}

次に、NetworkInterceptorインターフェースを実装するデフォルトのインターセプターDefaultInterceptorを実装します。

import { http } from '@kit.NetworkKit';
import { RequestOptions } from '../NetworkService';
import { LibLogManager, TAG } from '../LogService'; 

export class DefaultInterceptor implements NetworkInterceptor {

  beforeRequest(request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void {
    // ここでネットワークパラメーターを追加したり、リクエストを他の方法で処理したりできます
    httprequest.on('headersReceive', (header) => {
      LibLogManager.getLogger().info(TAG, '受信したヘッダー: ' + JSON.stringify(header));
    });

    // 非同期操作がある場合は、Promiseを返す必要があります
    // ここでは非同期操作がないため、そのまま返します
  }

  afterResponse(response: http.HttpResponse | object, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void {
    // レスポンス受信後、レスポンスデータを処理したり、ログを記録したりできます
    httprequest.off('headersReceive'); // イベントリスナーを削除
    LibLogManager.getLogger().info(TAG, 'レスポンス受信: ' + JSON.stringify(response));

    // 非同期操作がある場合は、Promiseを返す必要があります
    // ここでは非同期操作がないため、そのまま返します
  }

  onError(error: Error, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void {
    // エラーが発生した場合、エラーログを記録したり、エラー処理を行ったりできます
    httprequest.off('headersReceive'); // イベントリスナーを削除
    LibLogManager.getLogger().error(TAG, 'ネットワークエラーが発生しました: ' + JSON.stringify(error));

    // 非同期操作がある場合は、Promiseを返す必要があります
    // ここでは非同期操作がないため、そのまま返します
  }
}

注意

  1. 上記のbeforeRequestメソッドでは、headersReceiveイベントのリスナーを追加しました。

  2. afterResponseonErrorメソッドでは、httprequest.off('headersReceive')を呼び出して、以前に追加したイベントリスナーを削除しています。これは、メモリリークを避けるためです。新しいリクエストを繰り返し送信する場合、古いリスナーを削除しないと、これらのリスナーはメモリに常駐し続けます。

  3. 実際のプロジェクトでは、ネットワークライブラリやプロジェクトのニーズに応じて、これらのインターセプターの実装を調整する必要があるかもしれません。例えば、beforeRequestメソッドでは、リクエストヘッダー、認証トークンなどを追加する必要があるかもしれません。afterResponseメソッドでは、JSONレスポンスデータを処理したり、それを他の形式に変換したりする必要があるかもしれません。onErrorメソッドでは、より複雑なエラー処理ロジックを実行する必要があるかもしれません。例えば、リトライメカニズム、エラー報告などです。

五、ネットワークリクエストカプセル化のコアクラス

ネットワークプログラミングでは、HTTPリクエストを発行することは一般的なタスクです。このプロセスを簡素化し、より標準化され、保守しやすいようにするため、私たちはHTTPリクエストを発行するためのネットワークリクエストカプセル化のコアクラスを作成しました。このクラスは、柔軟なAPIを提供し、ユーザーが設定化された方法でさまざまなHTTPリクエストを発行することができます。

1. リクエスト設定クラス:RequestOptions

まず、HTTPリクエストを発行するためのすべての設定パラメーターを含むRequestOptionsインターフェースを定義しました。このインターフェースの設計は非常に柔軟で、さまざまな複雑なHTTPリクエストシナリオに対応することができます。

export interface RequestOptions {
  baseUrl?: string; // ベースURL
  act?: string; // リクエストのアクションまたはパス
  method?: RequestMethod; // リクエストメソッド、デフォルトはGET
  queryParams?: Map<string, any>; // クエリパラメーター、複数のデータタイプをサポート
  header?: Object; // リクエストヘッダー情報
  extraData?: string | Object | ArrayBuffer; // 追加のリクエストデータ
  expectDataType?: http.HttpDataType; // 予測されるレスポンスデータタイプ
  usingCache?: boolean; // キャッシュを使用するかどうか
  priority?: number; // リクエストの優先度
  connectTimeout?: number; // 接続タイムアウト
  readTimeout?: number; // 読み取りタイムアウト
  multiFormDataList?: Array<http.MultiFormData>; // POSTフォームリクエスト用のフォームデータリスト
}

2. リクエストメソッドの列挙:RequestMethod

さまざまなHTTPリクエストメソッドをサポートするため、RequestMethod列挙を定義しました。この列挙は、GET、POST、PUTなど、すべての標準的なHTTPリクエストメソッドを含んでいます。

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

3. ネットワークリクエストカプセル化のコアクラス:NetworkService

RequestOptionsRequestMethodに基づいて、ネットワークリクエストカプセル化のコアクラスNetworkServiceを作成しました。このクラスは、requestメソッドを提供し、HTTPリクエストを発行します。requestメソッドは、RequestOptionsオブジェクトを受け取り、その設定に従って対応するHTTPリクエストを発行します。

NetworkServiceクラスでは、インターセプター(Interceptor)の登録もサポートしています。インターセプターは、リクエストを送信する前に、レスポンスを受信した後に、追加の処理を行うことができます。例えば、リクエストヘッダーを追加したり、レスポンスデータを処理したりするなどです。これにより、ユーザーはネットワークリクエストの動作を柔軟にカスタマイズすることができます。


export class NetworkService {
  baseUrl:string;

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

  private interceptors: NetworkInterceptor[] = [];

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

  async request(requestOption: RequestOptions): Promise<http.HttpResponse | null> {
    let response: http.HttpResponse | null = null;
    let error: Error | null = null;
    // httpRequestは1つの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);
      }

      if(requestOption.baseUrl === null || requestOption.baseUrl.trim().length === 0){
        throw new NetworkError(NetworkServiceErrorConst.URL_NOT_EXIST_ERROR, Application.getInstance().resourceManager.getStringSync($r("app.string.invalid_url_not_exist")))
      }

      if (!LibNetworkStatus.getInstance().isNetworkAvailable()) {
        LibLogManager.getLogger().error("HttpCore","ネットワークが使用できません")
        throw new NetworkError(NetworkServiceErrorConst.UN_AVILABLE, Application.getInstance().resourceManager.getStringSync($r("app.string.network_unavailable")))
      }

      if (!this.isValidUrl(requestOption.baseUrl)) {
        LibLogManager.getLogger().error("HttpCore","URL形式が不正です")
        throw new NetworkError(NetworkServiceErrorConst.URL_ERROR, Application.getInstance().resourceManager.getStringSync($r("app.string.invalid_url_format")))
      }

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

      let response = await httpRequest.request(this.appendQueryParams(requestOption.baseUrl, requestOption.queryParams), {
        method: requestOption.method,
        header: requestOption.header || defalutHeader,
        extraData: requestOption.extraData, // POSTリクエストを使用する場合、このフィールドを使用して内容を渡します
        expectDataType: requestOption.expectDataType||http.HttpDataType.STRING, // オプション、返信データのタイプを指定します
        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) {
        response = response;
      } else{
        throw new NetworkResponseError(response.responseCode,Application.getInstance().resourceManager.getStringSync($r("app.string.network_unavailable")))
      }

      // インターセプターのafterResponseメソッドを呼び出す
      for (const interceptor of this.interceptors) {
        await interceptor.afterResponse(response, requestOption, httpRequest );
      }

    } catch (e) {
      error = e;
    }

    // エラーがあるかどうかに応じて、インターセプターの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 {
    
  }

  private appendQueryParams(url: string, queryParams: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >|undefined): string {
    
  }
}

NetworkServiceクラスを使用することで、ユーザーはHTTPリクエストを発行する方法をより専門的で簡潔にし、設定化、インターセプターなどの高度な機能がもたらす利便性を楽しむことができます。これにより、開発効率を向上させ、コードをより明確で、保守しやすくすることができます。

Discussion