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

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

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

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


王二蛋和他的狗
10 声望3 粉丝

Android开发高级工程师