HarmonyOS実践開発:ネットワーク層のアート——エレガントなカプセル化と構築ガイド(前編)
HarmonyOSの広大な開発の世界で、ネットワーク層は情報交換の橋渡しとして、その重要性は言うまでもありません。本日は、皆さんと一緒に、芸術的な手法で、HarmonyOS公式のネットワークライブラリをエレガントにカプセル化し、アプリケーションに効率的で柔軟なネットワーク層を構築する方法を探求します。次回の章では、この完全にカプセル化されたネットワークライブラリをどのように使用して、ネットワーク層の開発と使用を簡単に驾驭するかについて詳しく説明します。
一、カプセル化の目的:拡張性とインターセプション性
HarmonyOSアプリケーションの開発では、ネットワークリクエストのカプセル化は、開発プロセスを簡素化するためだけでなく、コードの再利用可能性と保守性を向上させるためでもあります。私たちのカプセル化目標は、主に以下の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を返す必要があります
// ここでは非同期操作がないため、そのまま返します
}
}
注意:
-
上記の
beforeRequest
メソッドでは、headersReceive
イベントのリスナーを追加しました。 -
afterResponse
とonError
メソッドでは、httprequest.off('headersReceive')
を呼び出して、以前に追加したイベントリスナーを削除しています。これは、メモリリークを避けるためです。新しいリクエストを繰り返し送信する場合、古いリスナーを削除しないと、これらのリスナーはメモリに常駐し続けます。 -
実際のプロジェクトでは、ネットワークライブラリやプロジェクトのニーズに応じて、これらのインターセプターの実装を調整する必要があるかもしれません。例えば、
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
RequestOptions
とRequestMethod
に基づいて、ネットワークリクエストカプセル化のコアクラス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