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

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容