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;
}
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('&');
}
}
Core Points Analysis:
- Parameter Type Support: Support for multiple types of parameters, including strings, numbers, booleans, and arrays.
-
Encoding Processing: Use
encodeURIComponent
to encode parameters to ensure their validity in the URL. -
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()
}
}
Core Points Analysis:
-
Content Type Identification: Clearly specify the content type supported by the converter through the
contentType()
method. - JSON Conversion: Convert JavaScript objects to JSON strings.
- 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")
}
}
Core Points Analysis:
-
Converter Registration: Register converters to the manager through the
registerRequestConverter
andregisterResponseConverter
methods. - Converter Selection: Automatically select the appropriate converter for processing based on the content type of the request or response.
- 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}`);
}
}
}
}
Core Points Analysis:
- Request Recording: Record the URL, method, header information, and request body of the request.
- Response Recording: Record the status code, header information, and response body of the response.
-
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
}
}
Core Points Analysis:
-
Default Value Setting: Provide default values for configuration items through the
??
operator. - 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)