DEV Community

zhonghua
zhonghua

Posted on • Edited on

Practical Development of HarmonyOS: The Art of Network Layer - Elegant Encapsulation and Construction Guide (Part II)

Practical Development of HarmonyOS: The Art of Network Layer - Elegant Encapsulation and Construction Guide (Part II)

Foreword
In the vast realm of HarmonyOS development, the construction and encapsulation of the network layer are undoubtedly the cornerstone for building efficient and stable applications. Following the exploration in the previous article, this paper will continue to delve into the optimization journey of the network layer, revealing how to elevate the art of network layer construction to a new level through type converters, request query appenders, and a wealth of constant parameters.

I. In-depth Optimization of Network Requests

Data Type Converter: Definition and Practice
In the world of network requests, data format conversion is of vital importance. By defining the DataConverter interface, we have achieved flexible conversion of request and response data types.

export interface DataConverter {
  requestConvert(extraData: string | Object | ArrayBuffer): string | Object | ArrayBuffer;
  responseConvert(data: string | Object | ArrayBuffer, responseType: http.HttpDataType): string | Object | ArrayBuffer;
}
Enter fullscreen mode Exit fullscreen mode

Default Data Converter: Implementation of JSON Converter
We have implemented a default JsonDataConverter, which converts request data into a JSON string and converts response data into the appropriate format based on the response type.


export class JsonDataConverter implements DataConverter {
  requestConvert(extraData: string | Object | ArrayBuffer): string | Object | ArrayBuffer {
    // Convert request data into a JSON string
    return JSONUtil.beanToJsonStr(extraData);
  }

  responseConvert(data: string | Object | ArrayBuffer, responseType: http.HttpDataType): string | Object | ArrayBuffer {
    // Convert response data into the corresponding format based on responseType
    switch (responseType) {
      case http.HttpDataType.STRING:
        return JSON.parse(data as string);
      case http.HttpDataType.OBJECT:
        return data;
      default:
        return data;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Parameter Appender: Flexible Reorganization of Request Data
The QueryParamAppender interface allows us to reorganize the sent request data to meet business needs such as parameter signing.

// Define an interface for appending query parameters
export interface QueryParamAppender {
  append(queryParams?: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >): string|undefined;
}
Enter fullscreen mode Exit fullscreen mode

Default Appender: Simplified Query Parameter Processing
Through the implementation of CustomQueryParamAppender, we have simplified the encoding and appending process of query parameters.


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); // Remove the last '&'
        }
      } else {
        encodedValue = encodeURIComponent(key) + '=' + encodeURIComponent(value.toString());
      }
      paramsArray.push(encodedValue);
    }
    return paramsArray.join('&');
  }

}
Enter fullscreen mode Exit fullscreen mode

II. Constant Definition: Building a Solid Foundation for the Network Layer
By defining a series of constants, we provide a unified interface for error handling of network requests. These constants not only cover various network error scenarios but also include the meanings of HTTP status codes, providing clear guidance for developers.

{
      "name": "network_unavailable",
      "value": "Network unavailable"
    },
    {
      "name": "invalid_url_format",
      "value": "Invalid URL format"
    },
    {
      "name": "invalid_url_not_exist",
      "value": "URL does not exist"
    },
    {
      "name": "parameter_error",
      "value": "Parameter error"
    },
    {
      "name": "permission_denied",
      "value": "Permission denied"
    },
    {
      "name": "unsupported_protocol",
      "value": "Unsupported protocol"
    },
    {
      "name": "bad_url_format",
      "value": "URL uses incorrect/illegal format or lacks URL"
    },
    {
      "name": "could_not_resolve_proxy_name",
      "value": "Failed to resolve proxy name"
    },
    {
      "name": "could_not_resolve_host_name",
      "value": "Failed to resolve host name"
    },
    {
      "name": "could_not_connect_to_server",
      "value": "Failed to connect to server"
    },
    {
      "name": "weird_server_reply",
      "value": "Server reply is abnormal"
    },
    {
      "name": "access_denied_to_remote_resource",
      "value": "Access to remote resource is denied"
    },
    {
      "name": "http2_framing_layer_error",
      "value": "HTTP2 framing layer error"
    },
    {
      "name": "transferred_partial_file",
      "value": "Partial file transferred"
    },
    {
      "name": "failed_writing_data_to_disk",
      "value": "Failed to write data to disk/application"
    },
    {
      "name": "upload_failed",
      "value": "Upload failed"
    },
    {
      "name": "failed_to_open_read_local_data",
      "value": "Failed to open/read local data"
    },
    {
      "name": "out_of_memory",
      "value": "Out of memory"
    },
    {
      "name": "timeout_reached",
      "value": "Timeout reached"
    },
    {
      "name": "redirects_exceeded",
      "value": "Reached the maximum number of redirects"
    },
    {
      "name": "server_returned_nothing",
      "value": "Server did not return any content (no headers, no data)"
    },
    {
      "name": "failed_sending_data_to_peer",
      "value": "Failed to send data to peer"
    },
    {
      "name": "failure_receiving_data_from_peer",
      "value": "Failed to receive data from peer"
    },
    {
      "name": "ssl_certificate_problem",
      "value": "Local SSL certificate problem"
    },
    {
      "name": "unsupported_ssl_cipher",
      "value": "Unsupported specified SSL encryption algorithm"
    },
    {
      "name": "ssl_peer_certificate_or_ssh_remote_key_not_ok",
      "value": "SSL peer certificate or SSH remote key is not correct"
    },
    {
      "name": "unrecognized_http_content_or_transfer_encoding",
      "value": "Unrecognized HTTP content or transfer encoding"
    },
    {
      "name": "maximum_file_size_exceeded",
      "value": "Exceeded maximum file size"
    },
    {
      "name": "disk_full_or_allocation_exceeded",
      "value": "Disk is full or allocation exceeds limit"
    },
    {
      "name": "remote_file_already_exists",
      "value": "Remote file already exists"
    },
    {
      "name": "ssl_ca_cert_problem",
      "value": "SSL CA certificate problem (path? Access permission?)"
    },
    {
      "name": "remote_file_not_found",
      "value": "Remote file not found"
    },
    {
      "name": "authentication_function_error",
      "value": "Authentication function returns an error"
    },
    {
      "name": "unknown_other_error",
      "value": "Unknown other error"
    },
    {
      "name": "bad_request",
      "value": "The client request syntax is incorrect, and the server cannot understand it."
    },
    {
      "name": "unauthorized",
      "value": "The request requires authentication."
    },
    {
      "name": "forbidden",
      "value": "The server understands the request from the client, but refuses to execute this request."
    },
    {
      "name": "not_found",
      "value": "The server cannot find the resource (web page) according to the client's request."
    },
    {
      "name": "method_not_allowed",
      "value": "The method in the client request is prohibited."
    },
    {
      "name": "request_timeout",
      "value": "Request timeout."
    },
    {
      "name": "unsupported_media_type",
      "value": "The server does not support the format of the request (e.g., the request contains a MIME type that the server does not support)."
    },
    {
      "name": "internal_server_error",
      "value": "The server encounters an internal error and cannot complete the request."
    },
    {
      "name": "bad_gateway",
      "value": "When the server acts as a gateway or proxy and attempts to execute the request, it receives an invalid response from the upstream server."
    },
    {
      "name": "service_unavailable",
      "value": "Due to overload or system maintenance, the server is currently unable to process the request."
    },
    {
      "name": "gateway_timeout",
      "value": "When theserver acts as a gateway or proxy and attempts to execute the request, it fails to receive the needed response from the upstream server in time."
}

Enter fullscreen mode Exit fullscreen mode

Usage of constant class code


import { Application } from '../../app/Application'
import { NetworkError } from '../../exception/NetworkError'

export class NetworkServiceErrorConst {
// Network unavailable
static readonly UN_AVILABLE: number = 100000
// URL error
static readonly URL_ERROR: number = 100001
// URL does not exist error
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, here we go:

Enter fullscreen mode Exit fullscreen mode
  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;
Enter fullscreen mode Exit fullscreen mode

}

}


III. Exception Definition: Clear Error Handling Strategy
We have re-packaged network request errors and defined classes such as `BaseError` and `NetworkError`, making the error types clear and easy for developers to quickly locate problems.

Enter fullscreen mode Exit fullscreen mode


typescript

// Custom error type
import { http } from '@kit.NetworkKit';

export abstract class BaseError extends Error{

}

// Basic network error
export class NetworkError extends BaseError {

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

// Network request code error
export class NetworkResponseError extends BaseError {

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


IV. Interceptor: The Guardian of Network Requests
By optimizing the interceptor interface, we can execute specific logic before and after the request is sent, as well as when an error occurs, such as log recording and permission verification.

Enter fullscreen mode Exit fullscreen mode

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


Default implementation of the interceptor:

Enter fullscreen mode Exit fullscreen mode

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"

// Create an object that conforms to the RequestOptions interface
const requestOptions: RequestOptions = {
baseUrl: 'https://api.example.com',
act: 'someAction'
};

export class DefaultInterceptor implements NetworkInterceptor {

beforeRequest(request: RequestOptions, httprequest: http.HttpRequest): void | Promise {
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 {
httprequest.off('headersReceive');
LibLogManager.getLogger().info(TAG,'response: ' + JSONUtil.beanToJsonStr(response));
}

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

}


V. Core Network Layer Code: The Heart of Network Service
In this section, we will demonstrate how to implement a powerful and flexible network request processing mechanism through the `NetworkService` class. This class integrates all core functions such as data conversion, parameter appending, and exception handling.

Enter fullscreen mode Exit fullscreen mode


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. Create RequestOptions.ets configuration class
export interface RequestOptions {
baseUrl?: string;
act?: string;
method?: RequestMethod; // default is GET
queryParams ?: Map | Array | Array >;
header?: Record | Map | HashMap;
extraData?: string | Object | ArrayBuffer;
expectDataType?: http.HttpDataType;
usingCache?: boolean;
priority?: number;
connectTimeout?: number;
readTimeout?: number;
multiFormDataList?:Array;
}

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; // Specify the converter

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

private _queryParamAppender: QueryParamAppender = new CustomQueryParamAppender(); // Specify the query parameter appending rule

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

private interceptors: NetworkInterceptor[] = [];

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

async request(requestOption: RequestOptions): Promise {
let response: http.HttpResponse | null = null;
let error: BaseError | null = null;
// Each httpRequest corresponds to an HTTP request task and cannot be reused
let httpRequest = http.createHttp();
// Start sending the request
try {

  // If the URL is passed in, use the passed-in URL
  requestOption.baseUrl = requestOption.baseUrl?requestOption.baseUrl:this.baseUrl;

  // Call the beforeRequest method of the interceptor
  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
Enter fullscreen mode Exit fullscreen mode

}
}

  // Use the converter to transform request data
  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","Network unavailable")
    throw NetworkServiceErrorConst.getNetworkError(NetworkServiceErrorConst.UN\_AVILABLE)
  }

  if (!this.isValidUrl(requestOption.baseUrl)) {
    LibLogManager.getLogger().error("HttpCore","Invalid URL format")
    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, // When using POST request, this field is used to pass content
    expectDataType: expectDataType, // Optional, specify the type of return data
    usingCache: requestOption.usingCache, // Optional, default is true
    priority: requestOption.priority, // Optional, default is 1
    connectTimeout: requestOption.connectTimeout, // Optional, default is 60000ms
    readTimeout: requestOption.readTimeout, // Optional, default is 60000ms
    multiFormDataList: requestOption.multiFormDataList,
  })

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

  // Use the converter to transform response data
  if (response && this.\_dataConverter) {
    response.result = this.\_dataConverter.responseConvert(response.result, expectDataType);
  }

  // Call the afterResponse method of the interceptor
  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)
  }
}

// Call the afterResponse or onError method of the interceptor based on whether there is an error
if (error) {
  for (const interceptor of this.interceptors) {
    await interceptor.onError(error, requestOption, httpRequest);
  }
  httpRequest.destroy();
  throw error; // Re-throw the error so that the caller can handle it
} else{
  httpRequest.destroy();
  return response!;
}
Enter fullscreen mode Exit fullscreen mode

}

private isValidUrl(url: string): boolean {
// Regular expression to match URL
const urlPattern = new RegExp(
'^(https?:\/\/)?' + // protocol
'(((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);
}

}




## Conclusion
This article has delved into the encapsulation and optimization of the network layer, from data conversion to error handling, each step reflects the art of building an efficient network service. It is hoped that these practices will help developers to be at ease in HarmonyOS development and build more robust and user-friendly applications.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)