Practical HarmonyOS Development: The Art of Network Layer - Elegant Encapsulation and Construction Guide (Part 1)
In the vast world of HarmonyOS development, the network layer, acting as a bridge for information exchange, is of undeniable importance. Today, I will guide you through how to artistically and elegantly encapsulate HarmonyOS's official network library to build an efficient and flexible network layer for our applications. In the next installment, we will delve into how to utilize this well-encapsulated network library to effortlessly master the development and usage of the network layer.
- Encapsulation Goals: Expandability and Interceptor Capability
In HarmonyOS application development, the encapsulation of network requests is not only to simplify the development process but also to enhance the reusability and maintainability of the code. Our encapsulation goals primarily revolve around the following two points:
- Expandability: Allows developers to easily extend the functionality of network requests according to business needs, such as adding custom request headers, setting request timeout durations, etc.
Interceptor Capability: Provides an interception mechanism for network requests, enabling us to perform a series of operations before the request is sent or after the response is received, such as adding log records, error handling, etc.
Defining Basic Elements: Error Constants and Strings
Error Constants Definition
To centrally manage error codes in network requests, we define a NetworkServiceErrorConst
class to store various error codes that network requests might encounter:
export class NetworkServiceErrorConst {
// Network unavailable
static readonly UN_AVAILABLE: number = 100000;
// URL error
static readonly URL_ERROR: number = 100001;
// URL does not exist error
static readonly URL_NOT_EXIST_ERROR: number = 100002;
// Network error
static readonly NET_ERROR: number = 100003;
// ...other possible error codes
}
- Error Strings Definition
Additionally, we need to define error strings corresponding to the error codes to display to users within the application:
{
"name": "network_unavailable",
"value": "Network unavailable"
},
{
"name": "invalid_url_format",
"value": "Invalid URL format"
},
{
"name": "invalid_url_not_exist",
"value": "URL does not exist"
}
- Utility Toolkit
URL Validation
To ensure the correctness of the URL format in network requests, we provide an isValidUrl
function that uses a regular expression to validate the validity of the URL.
private isValidUrl(url: string): boolean {
// Regular expression to match various possible URL formats
const urlPattern = new RegExp(
'^(https?:\\/\\/)?' + // Protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // Or IPv4 address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // Port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // Query string
'(\\#[-a-z\\d_]*)?$', // Fragment identifier
'i' // Case insensitive
);
return urlPattern.test(url); // Return validation result
}
// Usage example
if (isValidUrl("http://example.com")) {
console.log("URL is valid.");
} else {
console.log("URL is invalid.");
}
Query Parameter Concatenation
When needing to append query parameters to a URL, the appendQueryParams
function can help us achieve this easily. It supports handling parameters with single values or array values and automatically handles encoding.
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())}`);
}
});
// Check if the URL already contains query parameters and decide whether to use '?' or '&' as the separator accordingly
const separator = url.includes('?') ? '&' : '?';
return url + separator + paramsArray.join('&');
}
// Usage example
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); // Output: http://example.com/search?q=test&page=2
With the above two utility functions, we can ensure that the URLs of network requests are both correct and contain the required query parameters, thereby enhancing the accuracy and reliability of network requests.
- Writing Network Interceptors
In the process of network requests and responses, network interceptors (Interceptor) are a very important concept. They allow us to execute specific logic before the request is sent, after the response is received, or when an error occurs, such as adding network parameters, logging, handling errors, etc.
First, we define a NetworkInterceptor
interface that specifies the methods that interceptors must implement:
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;
}
Next, we implement a default interceptor DefaultInterceptor
that implements the NetworkInterceptor
interface:
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 {
// You can add network parameters here or perform other processing on the request
httprequest.on('headersReceive', (header) => {
LibLogManager.getLogger().info(TAG, 'Received headers: ' + JSON.stringify(header));
});
// If there are asynchronous operations, a Promise needs to be returned
// There are no asynchronous operations here, so return directly
}
afterResponse(response: http.HttpResponse | object, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void {
// After the response is received, you can process the response data or log it
httprequest.off('headersReceive'); // Remove event listener
LibLogManager.getLogger().info(TAG, 'Response received: ' + JSON.stringify(response));
// If there are asynchronous operations, a Promise needs to be returned
// There are no asynchronous operations here, so return directly
}
onError(error: Error, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void {
// When an error occurs, you can log the error or handle it
httprequest.off('headersReceive'); // Remove event listener
LibLogManager.getLogger().error(TAG, 'Network error occurred: ' + JSON.stringify(error));
// If there are asynchronous operations, a Promise needs to be returned
// There are no asynchronous operations here, so return directly
}
}
Notes:
In the
beforeRequest
method above, I added a listener for theheadersReceive
event.In the
afterResponse
andonError
methods, I calledhttprequest.off('headersReceive')
to remove the previously added event listener. This is to avoid memory leaks, as if you keep sending new requests without removing old listeners, these listeners will remain in memory indefinitely.In actual projects, you may need to adjust the implementation of these interceptors according to your network library and project requirements. For example, you might need to add request headers, authentication tokens, etc., in the
beforeRequest
method. In theafterResponse
method, you might need to process JSON response data or convert it to other formats. In theonError
method, you might need to implement more complex error handling logic, such as retry mechanisms, error reporting, etc.Core Network Request Encapsulation Class
In network programming, initiating HTTP requests is a common task. To simplify this process and make it more standardized and maintainable, we have created a core class for network request encapsulation. This class provides a set of flexible APIs that allow users to initiate various HTTP requests in a configuration-based manner.
- Request Configuration Class: RequestOptions
First, we define a RequestOptions
interface that includes all the configuration parameters required to initiate an HTTP request. The design of this interface is very flexible and can adapt to various complex HTTP request scenarios.
export interface RequestOptions {
baseUrl?: string; // Base URL
act?: string; // Request action or path
method?: RequestMethod; // Request method, default is GET
queryParams?: Map<string, any>; // Query parameters, supports various data types
header?: Object; // Request header information
extraData?: string | Object | ArrayBuffer; // Additional request data
expectDataType?: http.HttpDataType; // Expected response data type
usingCache?: boolean; // Whether to use cache
priority?: number; // Request priority
connectTimeout?: number; // Connection timeout
readTimeout?: number; // Read timeout
multiFormDataList?: Array<http.MultiFormData>; // List of form data for POST form requests
}
- Request Method Enumeration: RequestMethod
To support various HTTP request methods, we define a RequestMethod
enumeration. This enumeration includes all standard HTTP request methods, such as GET, POST, PUT, etc.
export enum RequestMethod {
OPTIONS = "OPTIONS",
GET = "GET",
HEAD = "HEAD",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE",
TRACE = "TRACE",
CONNECT = "CONNECT"
}
- Core Network Request Encapsulation Class: NetworkService
Based on RequestOptions
and RequestMethod
, we have created a core network request encapsulation class named NetworkService
. This class provides a request
method for initiating HTTP requests. The request
method accepts a RequestOptions
object as a parameter and initiates the corresponding HTTP request according to the configuration of this object.
In addition to the request
method, the NetworkService
class also supports registering interceptors (Interceptor). Interceptors can perform additional processing before the request is sent and after the response is returned, such as adding request headers, processing response data, etc. This allows users to flexibly customize the behavior of network requests.
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;
// Each httpRequest corresponds to an HTTP request task and is not reusable
let httpRequest = http.createHttp();
// Start sending the request
try {
// If the URL is provided, use the provided URL
requestOption.baseUrl = requestOption.baseUrl ? requestOption.baseUrl : this.baseUrl;
// Call the interceptor's beforeRequest method
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","Network unavailable")
throw new NetworkError(NetworkServiceErrorConst.UN_AVILABLE, Application.getInstance().resourceManager.getStringSync($r("app.string.network_unavailable")))
}
if (!this.isValidUrl(requestOption.baseUrl)) {
LibLogManager.getLogger().error("HttpCore","Invalid URL format")
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, // When using POST request, this field is used to pass content
expectDataType: requestOption.expectDataType||http.HttpDataType.STRING, // Optional, specify the type of response 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) {
response = response;
} else{
throw new NetworkResponseError(response.responseCode,Application.getInstance().resourceManager.getStringSync($r("app.string.network_unavailable")))
}
// Call the interceptor's afterResponse method
for (const interceptor of this.interceptors) {
await interceptor.afterResponse(response, requestOption, httpRequest );
}
} catch (e) {
error = e;
}
// Call the interceptor's afterResponse or onError method depending 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 the caller can handle it
} else{
httpRequest.destroy();
return response;
}
}
private isValidUrl(url: string): boolean {
}
private appendQueryParams(url: string, queryParams: Map<string, number|string|boolean|Array< number> | Array | Array >|undefined): string {
}
}
By using the NetworkService
class, users can initiate HTTP requests in a more professional and concise manner and enjoy the convenience brought by configuration-based, interceptor, and other advanced features. This not only improves development efficiency but also makes the code clearer and more maintainable.
6. Putting It All Together
Now that we have defined the core components of our network layer, let's see how they work together in a practical example. Suppose we want to make a GET request to fetch some data from a server. Here's how you can do it using the NetworkService
class:
// Create an instance of NetworkService with a base URL
const networkService = new NetworkService("http://api.example.com");
// Define request options
const requestOptions: RequestOptions = {
act: "/data", // The path to the resource
method: RequestMethod.GET, // The request method
queryParams: new Map<string, any>([
["param1", "value1"],
["param2", "value2"]
]), // Query parameters
header: {
"Authorization": "Bearer your_token_here"
} // Custom headers
};
// Make the request
networkService.request(requestOptions)
.then(response => {
if (response) {
console.log("Response received:", response.data);
} else {
console.log("No response received.");
}
})
.catch(error => {
console.error("Error occurred:", error.message);
});
In this example, we first create an instance of NetworkService
with a base URL. Then, we define the request options, including the path to the resource, the request method, query parameters, and custom headers. Finally, we call the request
method of NetworkService
to initiate the HTTP request and handle the response or error accordingly.
- Conclusion
In this part of the guide, we have explored the importance of the network layer in HarmonyOS development and how to encapsulate the official network library to create a flexible and efficient network layer. We have defined error constants and strings, provided utility functions for URL validation and query parameter concatenation, and created a core network request encapsulation class with support for interceptors.
In the next installment, we will dive deeper into how to use this well-encapsulated network library to handle various network scenarios, including error handling, caching, and more. Stay tuned for more insights into mastering the network layer in HarmonyOS development.
Top comments (0)