💭

HarmonyOS 運動プロジェクト開発:超便利な RCP ネットワークライブラリのカプセル化(前編)

に公開

HarmonyOS 運動プロジェクト開発:超便利な RCP ネットワークライブラリのカプセル化(前編)—— リクエストパラメータのカプセル化、タイプ変換器、ログ記録編

HarmonyOS 核心技術##運動開発## Remote Communication Kit(遠隔通信サービス)

HarmonyOS 運動プロジェクトの開発において、ネットワーク通信は欠かせない要素です。運動データの取得、ユーザー情報の同期、運動ビデオリソースのロードなど、安定性、効率性、そして使いやすさを兼ね備えたネットワークライブラリが必要です。本稿では、超便利な RCP ネットワークライブラリのカプセル化方法を深く探求し、HarmonyOS 開発における各種ネットワークリクエストを簡単に処理するお手伝いをします。本シリーズは前編、中編、後編の3部構成で、ネットワークライブラリのコア機能、高度な特性、実際のアプリケーションケースをそれぞれ紹介します。

まえがき

モバイルアプリケーションの開発において、ネットワークリクエストはバックエンドサービスとの通信の基本です。優れたネットワークライブラリは、基本的なリクエスト機能を提供するだけでなく、エラー処理、ログ記録、キャッシュ管理などの高度な特性も備える必要があります。HarmonyOS は、効率的なネットワーク通信を実現するための強力な RCP(Remote Communication Protocol)モジュールを提供しています。この RCP モジュールをカプセル化することで、機能が完全で使いやすいネットワークライブラリを構築し、開発効率とアプリケーションのパフォーマンスを向上させることができます。

  1. ネットワークライブラリのコア機能:リクエストとレスポンスの処理

(1)リクエストパラメータのカプセル化

ネットワークリクエストにおいて、パラメータの処理は重要な要素の一つです。複雑なリクエストパラメータ(フォームデータ、JSON オブジェクトなど)を適切な形式に変換する必要があります。このため、QueryParamAppender インターフェースを定義し、クエリパラメータの連結を処理する CustomQueryParamAppender クラスを実装しました。

// クエリパラメータを追加するインターフェースを定義する
export interface QueryParamAppender {
  append(queryParams?: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >|Record<string,string>| object |undefined): string|undefined;
}
import { QueryParamAppender } from "./QueryParamAppender";

export class CustomQueryParamAppender implements QueryParamAppender {
  append(queryParams?: Map<string, string | number | boolean | number[] | string[] | boolean[]> |Record<string,string> | object |undefined): string|undefined {
    if (queryParams === undefined || (queryParams instanceof Map && queryParams.size === 0) || (typeof queryParams === 'object' && Object.keys(queryParams).length === 0)) {
      return;
    }
    const paramsArray: string[] = [];
    // オブジェクトをキーと値のペアの配列に変換する
    let values:[string,string|number|boolean|number[]|string[]|boolean[]][] = Object.entries(queryParams)
    for (const qp of values) {
      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('&');
  }

}

コアポイントの解説:

  1. パラメータタイプのサポート:文字列、数字、ブール値、配列を含む複数のタイプのパラメータをサポートします。
  2. エンコード処理:encodeURIComponent を使用してパラメータをエンコードし、URL 内での合法性を確保します。
  3. 配列の処理:配列タイプのパラメータは、インデックスを使用して連結され、例えば key[0]=value1&key[1]=value2 のように処理されます。

(2)レスポンス内容の変換

ネットワークリクエストのレスポンス内容は、異なるコンテンツタイプ(JSON、テキストなど)に応じて変換する必要があります。このため、RequestConverterResponseConverter インターフェースを定義し、FormConverterJsonConverterTextResponseConverterObjectResponseConverter など、複数のコンバーターを実装しました。

import { rcp } from "@kit.RemoteCommunicationKit";
import { JSONUtil } from "../../JSONUtil";
import { modelToForm } from "../NetUtils";
import { RcpContentType } from "../RcpService";

export interface RequestConverter {
  contentType():RcpContentType;
  convert(value: object|undefined): rcp.RequestContent;
}

/**
 * Form フォームコンバーター
 */
export class FormConverter implements RequestConverter {
  contentType(): RcpContentType {
    return RcpContentType.FORM_URLENCODED
  }

  convert(value: object|undefined): rcp.RequestContent {
    return modelToForm(value);
  }
}

export class JsonConverter implements RequestConverter {

  contentType(): RcpContentType {
    return RcpContentType.JSON;
  }
  convert(value: object|undefined): rcp.RequestContent {
    if(value){
      return JSONUtil.toString(value);
    }
    return ''
  }
}

export interface ResponseConverter {
  contentType():RcpContentType;
  convert(response: rcp.Response): string|object|null;
}

/**
 * 元のテキストコンバーター
 */
export class TextResponseConverter implements ResponseConverter {
  contentType(): RcpContentType {
    return RcpContentType.TEXT_PLAIN;
  }
  convert(response: rcp.Response): string|object|null {
    return response.toString();
  }
}


export class ObjectResponseConverter implements ResponseConverter{
  contentType(): RcpContentType {
    return RcpContentType.JSON;
  }
  convert(response: rcp.Response): string|object|null {
    return response.toJSON()
  }
}

コアポイントの解説:

  1. コンテンツタイプの識別:contentType() メソッドを使用して、コンバーターがサポートするコンテンツタイプを明確に指定します。
  2. JSON 変換:JavaScript オブジェクトを JSON 文字列に変換します。
  3. テキストレスポンスの処理:レスポンス内容を直接文字列に変換します。

(3)リクエストとレスポンスの統一管理

リクエストとレスポンスをより効果的に管理するために、ConverterManager クラスを作成し、適切なコンバーターを登録し、選択するための機能を提供しました。


// src/main/ets/net/converter/ConverterManager.ts
import { rcp } from '@kit.RemoteCommunicationKit';
import { CustomErrorCode, LibError } from '../../LibError';
import { RcpContentType } from '../RcpService';
import { RequestConverter, ResponseConverter } from './RcpConverter';

/**
 * コンテンツタイプとコンバーターのマッピング
 */
export class ConverterManager {

  private requestConverters: RequestConverter[] = [];
  private responseConverters: ResponseConverter[] = [];
  private _requestFunc?: ((data: object|undefined, dataType: RcpContentType) => RequestConverter) | undefined;
  private _responseFunc?: ((response: rcp.Response) => ResponseConverter) | undefined;

  public set responseFunc(value: ((response: rcp.Response) => ResponseConverter) | undefined) {
    this._responseFunc = value;
  }

  public set requestFunc(value: ((data: object|undefined, dataType: string) => RequestConverter) | undefined) {
    this._requestFunc = value;
  }

  // リクエストコンバーターの登録
  public registerRequestConverter(converter: RequestConverter): void {
    this.requestConverters.push(converter);
  }

  // レスポンスコンバーターの登録
public registerResponseConverter(converter: ResponseConverter): void {
this.responseConverters.push(converter);
}

/
- リクエストデータに応じて自動的にコンバーターを選択する
   /
  selectRequestConverter(data: object|undefined,dataType: RcpContentType): rcp.RequestContent {
    if(this.requestFunc){
      return this.requestFunc(data , dataType).convert(data)
    }
    for(const request of this.requestConverters){
      if(request.contentType() == dataType){
        return request.convert(data);
      }
    }
    throw LibError.makeBusinessError(CustomErrorCode.NOT_FIND_REQUEST_CONVERTER_ERROR,"NOT_FIND_REQUEST_CONVERTER_ERROR")
  }

/
- レスポンスヘッダーに応じてコンバーターを選択する
   /
  selectResponseConverter(response: rcp.Response,contentType: string | string[] | undefined): string|object|null {
    if(this.responseFunc){
      return this.responseFunc(response).convert(response)
    }
    let dataType = RcpContentType.TEXT_PLAIN
    if(contentType){
      if (contentType.includes('application/json')) {
        dataType = RcpContentType.JSON
      } else if (contentType.includes('text/plain')) {
        dataType = RcpContentType.TEXT_PLAIN
      } else if (contentType.includes('text/html')) {
        dataType = RcpContentType.TEXT_PLAIN
      }
    }
    for(const responseConver of this.responseConverters){
      if(responseConver.contentType() == dataType){
        return responseConver.convert(response);
      }
    }
    throw LibError.makeBusinessError(CustomErrorCode.NOT_FIND_RESPONSE_CONVERTER_ERROR,"NOT_FIND_RESPONSE_CONVERTER_ERROR")
  }

}

コアポイントの解説

  1. コンバーターの登録registerRequestConverterregisterResponseConverter メソッドを使用して、コンバーターをマネージャーに登録します。
  2. コンバーターの選択:リクエストまたはレスポンスのコンテンツタイプに応じて、適切なコンバーターを自動的に選択して処理します。
  3. エラー処理:適切なコンバーターが見つからない場合、エラーをスローして通知します。

2. ネットワークライブラリの高度な特性:インターセプターとログ記録

(1)インターセプターのメカニズム

インターセプターは、ネットワークライブラリにおける重要な特性の一つで、リクエストの送信とレスポンスの返却時にカスタムロジックを挿入することができます。LoggingInterceptor を実装し、リクエストとレスポンスの詳細な情報を記録するための機能を提供しました。

import { rcp } from "@kit.RemoteCommunicationKit";
import { util } from "@kit.ArkTS";
import { appLogger } from "../../../app/Application";


export class LoggingInterceptor implements rcp.Interceptor {
  async intercept(context: rcp.RequestContext, next: rcp.RequestHandler): Promise<rcp.Response> {
    // リクエスト情報を記録する
    this.logRequest(context.request);

    // 次のリクエストハンドラーを呼び出す
    const response = await next.handle(context);

    // レスポンス情報を記録する
    this.logResponse(response);

    return response;
  }

  private logRequest(request: rcp.Request) {
    const method = request.method;
    const url = request.url.href;
    const headers = request.headers;
    const body = request.content;

    appLogger.info(`[リクエスト] ${method} ${url}`);
    appLogger.info(`[リクエストヘッダー] ${JSON.stringify(headers, null, 2)}`);

    if (body instanceof rcp.Form) {
      appLogger.info(`[リクエストボディ] ${JSON.stringify(body, null, 2)}`);
    } else {
      appLogger.info(`[リクエストボディ] ${body}`);
    }
  }

  private logResponse(response: rcp.Response) {
    const statusCode = response.statusCode;
    const headers = response.headers;
    const body = response.body;

    appLogger.info(`[レスポンス] ステータスコード: ${statusCode}`);
    appLogger.info(`[レスポンスヘッダー] ${JSON.stringify(headers, null, 2)}`);

    if (body) {
      try {
        const uint8Array = new Uint8Array(body);
        // ArrayBuffer を文字列に変換する
        const decoder = new util.TextDecoder();
        const bodyString = decoder.decodeToString(uint8Array);
        // JSON としてパースを試みる
        appLogger.logMultiLine(`[レスポンスボディ] ${JSON.stringify(JSON.parse(bodyString), null, 2)}`)

      } catch (error) {
        appLogger.logMultiLine(`[レスポンスボディ] ${body}`);
      }
    }
  }
}

コアポイントの解説:

  1. リクエストの記録:リクエストの URL、メソッド、ヘッダー、リクエストボディを記録します。
  2. レスポンスの記録:レスポンスのステータスコード、ヘッダー、レスポンスボディを記録します。
  3. ログのフォーマット化:JSON.stringify を使用してログをフォーマット化し、読みやすさを向上させます。

(2)ログ記録の重要性

ログ記録は、ネットワークリクエストのデバッグにおいて非常に重要です。リクエストとレスポンスの詳細な情報を記録することで、以下の問題を迅速に特定することができます。

  • リクエストパラメータが正しく伝送されているか。
  • レスポンスデータが期待どおりかどうか。
  • ネットワークリクエストがタイムアウトまたは失敗していないか。

実際の開発では、開発段階では詳細なログ記録を有効にし、本番環境ではログ記録を無効にしたり、重要な情報のみを記録したりすることで、パフォーマンスの低下を避けることをお勧めします。

  1. ネットワークライブラリの設定と初期化

ネットワークライブラリをより柔軟に使用するため、HttpConfig クラスを提供し、接続タイムアウト、転送タイムアウト、並行リクエストの最大数などのパラメータを設定することができます。

import { Timeout } from "./NetConstants";

// 明確な設定インターフェースを定義する
export interface IHttpConfigOptions {
  connectTimeout?: number;
  transferTimeout?: number;
  maxConcurrentRequests?: number;
  security?: boolean;

}

export class HttpConfig {
  /**
   * 接続タイムアウト(ミリ秒)
   */
  connectTimeout: number;

  /**
   * 転送タイムアウト(ミリ秒)
   */
  transferTimeout: number;

  /**
   * 最大並行リクエスト数
   */
  maxConcurrentRequests: number;

  constructor(config?: IHttpConfigOptions) {
    this.connectTimeout = config?.connectTimeout ?? Timeout.CONNECT_TIME_OUT;
    this.transferTimeout = config?.transferTimeout ?? Timeout.TRANS_TIME_OUT;
    this.maxConcurrentRequests = config?.maxConcurrentRequests ?? 5;
    this.security = config?.security ?? true
  }
}

コアポイントの解説:

  1. デフォルト値の設定:?? 演算子を使用して、設定項目にデフォルト値を提供します。
  2. 柔軟な設定:開発者が接続タイムアウト、並行リクエストの最大数などのパラメータを必要に応じてカスタマイズできるようにします。

本稿では、ネットワークライブラリのコア機能、リクエストパラメータのカプセル化、レスポンス内容の変換、インターセプターとログ記録のメカニズムについて詳しく紹介しました。これらの機能は、ネットワークライブラリの堅牢性と使いやすさを向上させるための基盤を提供します。次回の記事では、エラー処理、セッション管理、ネットワークステータスの検出など、ネットワークライブラリの高度な特性についてさらに詳しく探求し、ネットワークライブラリの堅牢性と使いやすさを向上させます。乞うご期待!

Discussion