DEV Community

zhonghua
zhonghua

Posted on • Edited on

HarmonyOS Sports Project Development: Packaging an Extremely Useful RCP Network Library (Part 1)

Core Technologies of HarmonyOS ## Sports Development ## Remote Communication Kit (Remote Communication Services)

In HarmonyOS sports project development, network communication is an indispensable part. Whether it is obtaining sports data, synchronizing user information, or loading sports video resources, a stable, efficient, and easy-to-use network library is required. This article will take you deep into how to package an extremely useful RCP network library to help you easily deal with various network requests in HarmonyOS development. This series of articles is divided into three parts: upper, middle, and lower, which introduce the core functions of the network library, advanced features, and practical application cases respectively.

Preface

In mobile application development, network requests are the basis for interacting with backend services. A good network library not only needs to provide basic request functions but also needs to have advanced features such as error handling, log recording, and cache management. The HarmonyOS system provides a powerful RCP (Remote Communication Protocol) module for efficient network communication. By packaging the RCP module, we can build a complete and easy-to-use network library to improve development efficiency and application performance.

I. Core Functions of the Network Library: Request and Response Processing

(I) Packaging of Request Parameters

In network requests, parameter processing is one of the key links. We need to convert complex request parameters (such as form data, JSON objects, etc.) into a format suitable for transmission. To this end, we have defined a QueryParamAppender interface and implemented the CustomQueryParamAppender class to handle the concatenation of query parameters.

// Define an interface for appending query parameters
export interface QueryParamAppender {
  append(queryParams?: Map<string, number|string|boolean|Array<number> | Array<string> | Array<boolean> >|Record<string,string>| object |undefined): string|undefined;
}
Enter fullscreen mode Exit fullscreen mode
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[] = [];
    // Use Object.entries() to convert the object into a key-value pair array
    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); // Remove the last '&'
        }
      } else {
        encodedValue = encodeURIComponent(key) + '=' + encodeURIComponent(value.toString());
      }
      paramsArray.push(encodedValue);
    }
    return paramsArray.join('&');
  }

}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Parameter Type Support: Support for multiple types of parameters, including strings, numbers, booleans, and arrays.
  2. Encoding Processing: Use encodeURIComponent to encode parameters to ensure their validity in the URL.
  3. Array Processing: For array-type parameters, concatenate through indexes, for example, key[0]=value1&key[1]=value2.

(II) Conversion of Response Content

The response content of network requests usually needs to be converted according to different content types (such as JSON, text, etc.). To this end, we have defined the RequestConverter and ResponseConverter interfaces and implemented various converters, such as FormConverter, JsonConverter, TextResponseConverter, and ObjectResponseConverter.

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 Converter
 */
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;
}

/**
 * Raw Text Converter
 */
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()
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Content Type Identification: Clearly specify the content type supported by the converter through the contentType() method.
  2. JSON Conversion: Convert JavaScript objects to JSON strings.
  3. Text Response Processing: Directly convert response content to a string.

(III) Unified Management of Requests and Responses

To better manage requests and responses, we have created the ConverterManager class to register and select appropriate converters.


// 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';

/**
 * Content type and converter mapping
 */
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;
  }

  // Register request converter
  public registerRequestConverter(converter: RequestConverter): void {
    this.requestConverters.push(converter);
  }

  // Register response converter
  public registerResponseConverter(converter: ResponseConverter): void {
    this.responseConverters.push(converter);
  }

  /**
   * Automatically select a converter based on request data
   */
  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")
  }

  /**
   * Select a converter based on response headers
   */
  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")
  }

}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Converter Registration: Register converters to the manager through the registerRequestConverter and registerResponseConverter methods.
  2. Converter Selection: Automatically select the appropriate converter for processing based on the content type of the request or response.
  3. Error Handling: Throw an error prompt if a suitable converter is not found.

II. Advanced Features of the Network Library: Interceptors and Log Recording

(I) Interceptor Mechanism

Interceptors are an important feature in the network library, used to insert custom logic when requests are sent and responses are returned. We have implemented the LoggingInterceptor to record detailed information of requests and responses.

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> {
    // Record request information
    this.logRequest(context.request);

    // Call the next request handler
    const response = await next.handle(context);

    // Record response information
    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(`[Request] ${method} ${url}`);
    appLogger.info(`[Request Headers] ${JSON.stringify(headers, null, 2)}`);

    if (body instanceof rcp.Form) {
      appLogger.info(`[Request Body] ${JSON.stringify(body, null, 2)}`);
    } else {
      appLogger.info(`[Request Body] ${body}`);
    }
  }

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

    appLogger.info(`[Response] Status Code: ${statusCode}`);
    appLogger.info(`[Response Headers] ${JSON.stringify(headers, null, 2)}`);

    if (body) {
      try {
        const uint8Array = new Uint8Array(body);
        // Convert ArrayBuffer to string
        const decoder = new util.TextDecoder();
        const bodyString = decoder.decodeToString(uint8Array);
        // Try to parse as JSON
        appLogger.logMultiLine(`[Response Body] ${JSON.stringify(JSON.parse(bodyString), null, 2)}`)

      } catch (error) {
        appLogger.logMultiLine(`[Response Body] ${body}`);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Request Recording: Record the URL, method, header information, and request body of the request.
  2. Response Recording: Record the status code, header information, and response body of the response.
  3. Log Formatting: Use JSON.stringify to format log output for easy reading.

(II) The Importance of Log Recording

Log recording is crucial in network request debugging. By recording detailed information of requests and responses, we can quickly locate problems, such as:

  • Whether the request parameters are correctly passed.
  • Whether the response data meets expectations.
  • Whether the network request times out or fails.

In actual development, it is recommended to enable detailed log recording during the development stage, and to turn it off or only record key information in the production environment to avoid performance loss.

III. Configuration and Initialization of the Network Library

To make the network library more flexible, we provide the HttpConfig class to configure parameters such as connection timeout, transfer timeout, and concurrent request limits.

import { Timeout } from "./NetConstants";

// Define a clear configuration interface
export interface IHttpConfigOptions {
  connectTimeout?: number;
  transferTimeout?: number;
  maxConcurrentRequests?: number;
  security?: boolean;

}

export class HttpConfig {
  /**
   * Connection timeout (milliseconds)
   */
  connectTimeout: number;

  /**
   * Transfer timeout (milliseconds)
   */
  transferTimeout: number;

  /**
   * Maximum number of concurrent requests
   */
  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
  }
}
Enter fullscreen mode Exit fullscreen mode

Core Points Analysis:

  1. Default Value Setting: Provide default values for configuration items through the ?? operator.
  2. Flexible Configuration: Allow developers to customize timeout, concurrency limits, and other parameters according to their needs.

In this part, we have introduced in detail the core functions of the network library, including request parameter packaging, response content conversion, and the interceptor and log recording mechanism. These functions provide a solid foundation for our network library. In the upcoming middle part, we will continue to delve into the advanced features of the network library, including error handling, session management, and network status detection, to further enhance the robustness and usability of the network library. Stay tuned!

Top comments (0)