写点什么

鸿蒙实战开发:网络层的艺术——优雅封装与搭建指南(上)

  • 2024-11-04
    北京
  • 本文字数:5918 字

    阅读完需:约 19 分钟

在鸿蒙的广袤开发世界中,网络层作为信息交换的桥梁,其重要性不言而喻。今天,我将带领大家一同探索如何以艺术般的手法,优雅地封装鸿蒙官方的网络库,为我们的应用搭建一个高效、灵活的网络层。我们在下一篇章中,将深入阐述如何利用这一封装完善的网络库,轻松驾驭网络层的开发与使用。

一、封装目的:可拓展与可拦截

在鸿蒙应用开发中,网络请求的封装不仅是为了简化开发流程,更是为了提高代码的复用性和可维护性。我们的封装目标主要围绕以下两点:


  1. 可拓展性:允许开发者根据业务需求,轻松扩展网络请求的功能,如添加自定义请求头、设置请求超时时间等。

  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 格式正确,我们提供了一个isValidUrl函数,它使用正则表达式来验证 URL 的有效性。


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 is valid.");} else { console.log("URL is invalid.");}
复制代码

参数拼接

当需要在 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
复制代码


通过上述两个工具函数,我们可以确保网络请求的 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;}
复制代码


接下来,我们实现一个默认的拦截器DefaultInterceptor,它实现了NetworkInterceptor接口:


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, 'Received headers: ' + JSON.stringify(header)); });
// 如果有异步操作,需要返回Promise // 这里没有异步操作,所以直接返回 }
afterResponse(response: http.HttpResponse | object, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void { // 响应接收后,可以处理响应数据,或者记录日志 httprequest.off('headersReceive'); // 移除事件监听器 LibLogManager.getLogger().info(TAG, 'Response received: ' + JSON.stringify(response));
// 如果有异步操作,需要返回Promise // 这里没有异步操作,所以直接返回 }
onError(error: Error, request: RequestOptions, httprequest: http.HttpRequest): Promise<void> | void { // 发生错误时,可以记录错误日志,或者进行错误处理 httprequest.off('headersReceive'); // 移除事件监听器 LibLogManager.getLogger().error(TAG, 'Network error occurred: ' + JSON.stringify(error));
// 如果有异步操作,需要返回Promise // 这里没有异步操作,所以直接返回 }}
复制代码


注意


  1. 在上面的beforeRequest方法中,我添加了一个对headersReceive事件的监听。

  2. afterResponseonError方法中,我调用了httprequest.off('headersReceive')来移除之前添加的事件监听器。这是为了避免内存泄漏,因为如果你不断发送新的请求而不移除旧的监听器,那么这些监听器将一直存在于内存中。

  3. 在实际项目中,你可能需要根据你的网络库和项目的需求来调整这些拦截器的实现。例如,你可能需要在beforeRequest方法中添加请求头、身份验证令牌等。在afterResponse方法中,你可能需要处理 JSON 响应数据,或者将其转换为其他格式。在onError方法中,你可能需要执行更复杂的错误处理逻辑,比如重试机制、错误上报等。


五、网络请求封装核心类


在网络编程中,发起 HTTP 请求是一项常见的任务。为了简化这一过程并使其更加标准化和可维护,我们创建了一个网络请求封装的核心类。这个类提供了一套灵活的 API,允许用户通过配置化的方式发起各种 HTTP 请求。


1. 请求配置类:RequestOptions


首先,我们定义了一个RequestOptions接口,它包含了发起 HTTP 请求所需的所有配置参数。这个接口的设计非常灵活,可以适应各种复杂的 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枚举。这个枚举包含了所有标准的 HTTP 请求方法,如 GET、POST、PUT 等。


export enum RequestMethod {  OPTIONS = "OPTIONS",  GET = "GET",  HEAD = "HEAD",  POST = "POST",  PUT = "PUT",  DELETE = "DELETE",  TRACE = "TRACE",  CONNECT = "CONNECT"}
复制代码


3. 网络请求封装核心类:NetworkService


基于RequestOptionsRequestMethod,我们创建了一个名为NetworkService的网络请求封装核心类。这个类提供了request方法,用于发起 HTTP 请求。request方法接受一个RequestOptions对象作为参数,并根据该对象的配置发起相应的 HTTP 请求。


除了request方法外,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对应一个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 请求,并享受到配置化、拦截器等高级功能带来的便利。这不仅可以提高开发效率,还可以使代码更加清晰、易于维护。

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2021-11-19 加入

还未添加个人简介

评论

发布
暂无评论
鸿蒙实战开发:网络层的艺术——优雅封装与搭建指南(上)_鸿蒙_王二蛋和他的张大花_InfoQ写作社区