HarmonyOS--基于Rcp的网络请求框架

285 阅读9分钟

哇袄!!

大家好啊,今天来点大家想看的东西,说的鸿蒙之RCP

RCP概念

这里引用官方原文来解释下RCP是什么:Remote Communication Kit中的@hms.collaboration.rcp(后续简称RCP)指的是远程通信平台(remote communication platform),RCP提供了网络数据请求功能,相较于Network Kit中HTTP请求能力,RCP更具易用性,且拥有更多的功能。在开发过程中,如果有些场景使用Network Kit中HTTP请求能力达不到预期或无法实现,那么就可以尝试使用RCP中的数据请求功能来实现。

简单来说是一个比官方Network Kit中HTTP更加强大且全面的网络请求平台,二者的对比以及更多内容详情请见官方文档《基于RCP的网络请求开发实践》

想直接看过程的直接去封装过程查看。

RCP的工作流程

以较为常见的GET方法为例子,RCP完成这一次网络请求的过程如下:

流程图:

流程图

时序图:

时序图

RCP组件简介

Session

Session类表示可用于发出HTTP请求的通信会话。它提供了各种HTTP方法(FETCH、GET、POST、PUT、HEAD、DELETE、CANCEL、CLOSE),可以通过API进行会话管理(创建/关闭对话),可以使用rcp.createSession方法创建Session对象(注:可创建Session对象上线为16个,超出则会报错,错误码:1007900994,报错信息:Sessions number reached limit。),cancel/close取消/关闭对话。

SessionConfiguration

createSession(sessionConfiguration?: SessionConfiguration): Session,接收参数为SessionConfiguration接口,该接口定义了本次会话的配置参数,接口具体属性如下:

名称类型只读可选说明
interceptorsInterceptor[]请求/响应拦截器。起始版本: 5.0.0(12)
requestConfigurationConfiguration指定与会话关联的HTTP请求的配置。包括transfer、proxy、DNS、connection和security configurations。
baseAddressURLOrString设置会话中URL的基地址。这允许开发者为会话中的多个请求定义一个通用的基本URL。如果请求URL不是绝对URL,则把基地址预制在请求URL的前面。例如,"example.com?name=value",example.com为基地址,"?name=value"为请求URL。
headersRequestHeaders为Session发出的HTTP请求定义headers(可自定义)。开发人员可以根据他们的需求定制的特定headers。
cookiesRequestCookies提供在与会话关联的HTTP请求中包含自定义cookie的方法。适用于需要将某些cookie附加到每个请求的场景。需要手动设置cookie,携带cookie为用户行为。
sessionListenerSessionListener允许开发人员将侦听器附加到会话,接收会话取消或关闭等事件的通知,更好地处理应用程序中与会话相关的事件。
connectionConfigurationConnectionConfiguration连接配置。用于指定此会话中允许的并发TCP连接总数以及单个主机所允许的最大并发TCP 连接数。起始版本: 5.0.0(12) 单个主机允许最大并发TCP连接数:默认6,最大2147483647; 允许最大同时TCP连接总数:默认64,最大2147483647;

简单归类为以下几个部分:

  • 全局配置:interceptors、sessionListener、connectionConfiguration
  • 基础类型配置:baseAddress、headers、cookies
  • 请求配置:requestConfiguration

Request

Request类表示一个HTTP请求,可以构造具体的请求信息,类属性如下:

名称类型只读可选说明
idstring请求对象的唯一标识符。由系统生成。
urlURLHTTP请求的URL。
methodHttpMethod要使用的HTTP方法。默认值为'GET'。
headersRequestHeadersHTTP请求头。默认值为undefined。
contentRequestContentHTTP请求内容。默认值为undefined。
cookiesRequestCookiesHTTP请求的Cookie。将设置转换为HTTP Cookies标头。默认值为undefined。
transferRangeTransferRange/TransferRange[]HTTP传输范围。转换为HTTP Range头。带有range头的HTTP请求要求服务器只返回HTTP响应的一部分。默认值为undefined。
configurationConfigurationHTTP请求配置。用于覆盖默认或会话范围的设置。默认值为undefined。
destinationResponseBodyDestinationHTTP响应体放置位置。起始版本:5.0.0(12)

简单归类为以下几个部分:

  • 必须属性:id、url、method
  • 特有属性:content、transferRange、destination
  • 可覆盖属性:headers、cookies、configuration

Configuration

Configuration接口包含一组配置参数,是http请求的配置对象,调整会话中http请求的行为包括传输、跟踪、代理、DNS和安全配置等的设置。

接口具体属性如下:

名称类型只读可选说明
transferTransferConfiguration配置与HTTP请求期间的数据传输相关的各个方面,例如自动重定向和超时设置。
tracingTracingConfiguration使开发人员能够在HTTP请求期间捕获详细的跟踪信息,有助于调试和性能分析。
proxyProxyConfiguration配置会话的代理设置,允许开发人员定义'system'、'no-proxy'或WebProxy配置。
dnsDnsConfiguration指定与DNS相关的配置,包括自定义DNS规则和通过HTTPS使用DNS的选项。
securitySecurityConfiguration包括用于处理安全相关方面的设置,如证书和服务器身份验证。
processingProcessingConfiguration响应处理配置。起始版本:5.0.0(12)

在上述材料中可以看到,RequestSession中都存在Configuration参数,Configuration在这两者中的使用准则如下:

ConfigurationRequest中设置为准,如果Request没有设置,则以Session为准。

组装参数流程图:

组装参数流程

Response

Response接口表示为http请求的响应对象,包含了请求的响应数据,具体属性如下:

名称类型只读可选说明
requestRequest收到此响应的相关HTTP请求内容。
statusCodenumberHTTP请求的结果代码。如果回调函数成功执行,将返回ResponseCode中定义的结果代码。否则,将在AsyncCallback中的error字段中返回错误代码。
headersResponseHeaders响应头。
effectiveUrlURL重定向后请求的有效URL。默认值为undefined。
bodyArrayBuffer响应内容根据响应头中的Content-type返回。响应内容必须与服务器返回的数据类型相同。
downloadedToDownloadedTo内容下载的路径。在使用下载至文件对象和文件标识符两种方式时,默认情况下,并未设置回调信息。起始版本:5.0.0(12)
debugInfoDebugInfo[]与响应相关联的调试信息。默认值为undefined。
timeInfoTimeInfoHTTP请求各阶段的定时信息。默认值为undefined。
cookiesResponseCookie[]响应中的Cookie数组。
httpVersionHttpVersionHTTP的版本。起始版本:5.0.0(12)
reasonPhrasestringHTTP响应状态行的reasonPhrase,提供与数字状态代码相关的文本描述。起始版本:5.0.0(12)

简单归类为以下几个部分:

  • 基础属性:request、statusCode、headers、body、httpVersion、reasonPhrase
  • 其他属性: 除了基础属性的属性

封装过程

本次网络请求使用的API为wanAndroid中的开放API

UML

  • 类图

    类图

  • 组件图

    组件图

  • 流程图(以get为例) 流程图

网络层

RcpClient

RcpClient使用全局session,负责在发起网络请求之前需要获取Session以及配置Seesion

const TAG: string = 'SessionConfigManager'

//默认拦截器
export let defaultInterceptors: rcp.Interceptor[] = [
 //注册拦截器
]

//默认session config
export let defaultSessionConfig: rcp.SessionConfiguration = {
  interceptors: defaultInterceptors, //默认拦截器
  baseAddress: 'https://wanandroid.com/',
  sessionListener: {
    onCanceled: () => LogUtil.d(TAG, `Session was cancelled`),
    onClosed: () => LogUtil.d(TAG, `Session was closed`)
  },
  //配置全局请求config
  requestConfiguration: {
    tracing: {
      verbose: true,
      collectTimeInfo: true,
    },
    transfer: {
      timeout: {
        connectMs: 60 * 1000
      }
    }
  }
}

//管理当前session以及其config
export class RcpClient {
  private session: rcp.Session
  private currentConfig: rcp.SessionConfiguration = defaultSessionConfig

  constructor() {
    //使用默认config
    this.session = rcp.createSession(defaultSessionConfig)
  }

  addInterceptor(interceptor: rcp.Interceptor) {
    this.currentConfig.interceptors?.push(interceptor)
  }

  setSessionListener(listener: rcp.SessionListener) {
    this.currentConfig.sessionListener = listener
  }

  setConnectionConfiguration(connectionConfig: rcp.ConnectionConfiguration) {
    this.currentConfig.connectionConfiguration = connectionConfig
  }


  getInterceptors(): rcp.Interceptor[] {
    return this.currentConfig.interceptors ?? []
  }
  //获取当前config
  getDefaultConfig(): rcp.SessionConfiguration {
    return this.currentConfig
  }
  //获取当前session
  getSession(): rcp.Session {
    return this.session
  }

  cancel(requestToCancel?: rcp.Request | rcp.Request[]): void {
    this.session.cancel(requestToCancel)
  }

  close(): void {
    this.session.close()
  }
}

RequestConfig

RequestConfig作为自定义请求接口,除了正常request需要的参数之外还可以根据业务需求添加字段。

import { rcp } from '@kit.RemoteCommunicationKit'

export interface RequestConfig {
  url: rcp.URLOrString
  method: rcp.HttpMethod
  headers?: rcp.RequestHeaders
  content?: rcp.RequestContent
  cookies?: rcp.RequestCookies
  transferRange?: rcp.TransferRange | rcp.TransferRange[]
  configuration?: rcp.Configuration
  destination?: rcp.ResponseBodyDestination
  shotToast?: boolean
}

BaseModel

BaseModel作为接收Response数据类

export class BaseModel<T> {
  errorMsg: string = ""
  errorCode: number = -1
  data?: T
}

HttpCall

HttpCall为网络请求单例类,外部通过export的HttpCall实例对象发起网络请求,使用BaseModel作为基类数据类接收泛型数据T,发起网络请求前通过NetMonitor检查当前网络是否可用,根据自定义接口RequestConfig.showToast判断请求后是否需要吐司,使用rcp.TimeInfo打印请求过程中的网络阶段耗时,最后exprot全局对象httpCall暴露给业务层使用。

const TAG: string = 'HttpCall'
class HttpCall {
  private static httpCall?: HttpCall
  private client: RcpClient = new RcpClient()

  private constructor() {
  }

  static getInstance() {
    if (!HttpCall.httpCall) {
      HttpCall.httpCall = new HttpCall()
    }
    return HttpCall.httpCall
  }

  get<T = Object>(url: rcp.URLOrString, param?: Object, showToast?: boolean): Promise<BaseModel<T>> {
    return this.request({
      url: url,
      method: 'GET',
      headers: { "content-type": 'application/json' },
      content: param,
      shotToast: showToast
    })
  }

  post<T = Object>(url: rcp.URLOrString, param?: Object, showToast?: boolean): Promise<BaseModel<T>> {
    return this.request({
      url: url,
      method: 'POST',
      headers: { "content-type": 'application/x-www-form-urlencoded' },
      content: param,
      shotToast: showToast
    })
  }

  request<T = Object>(requestConfig: RequestConfig): Promise<BaseModel<T>> {
    return new Promise((resolve) => {
      LogUtil.d(TAG, '请求开始')
      //检查当前网络是否可用
      if (!NetMonitor.getInstance().isNetWorkAvailable()) {
        return
      }
      //TODO 请求参数处理

      this.client.getSession().fetch(this.createCustomRequest(requestConfig)).then((response: rcp.Response) => {
        LogUtil.d(TAG, JSON.stringify(response.request))
        if (response.statusCode != 200) {
          LogUtil.d(TAG, `statusCode error ${response.statusCode}`)
          return
        }
        this.printTimeInfo(response.timeInfo)
        let resData = response.toJSON() as BaseModel<T>
        //是否需要Toast
        if (requestConfig.shotToast && (resData.errorMsg.length != 0)) {
          ToastUtil.showToast(resData.errorMsg)
        }
        if (resData) {
          LogUtil.d(TAG, `请求成功: ${response.toString()}`)
          resolve(resData)
        } else {
          LogUtil.d(TAG, `response 异常`)
        }
      })
    })
  }

  private printTimeInfo(info?: rcp.TimeInfo) {
    if (!info) {
      LogUtil.d(TAG, `timeInfo is undefined`)
      return;
    }
    let remainderDataTime = info?.totalTimeMs - info?.startTransferTimeMs;
    let firstPackageTime = info?.startTransferTimeMs - info?.preTransferTimeMs;
    let TLSTime = info?.tlsHandshakeTimeMs - info?.connectTimeMs;
    LogUtil.d(TAG, `首包耗时${firstPackageTime}`)
    LogUtil.d(TAG, `TLS握手(不包含建连时间)耗时${TLSTime}`)
    LogUtil.d(TAG, `接收剩余数据的耗时${remainderDataTime}`)
  }

  private createCustomRequest(requestConfig: RequestConfig): rcp.Request {
    const req: rcp.Request = new rcp.Request(requestConfig.url, requestConfig.method)
    req.headers = requestConfig.headers
    req.content = requestConfig.content
    req.cookies = requestConfig.cookies
    req.transferRange = requestConfig.transferRange
    req.configuration = requestConfig.configuration
    req.destination = requestConfig.destination
    return req
  }

  getClient(): RcpClient {
    return this.client
  }
}

//对外唯一对象
export let httpCall: HttpCall = HttpCall.getInstance()

NetMonitor

NetMonitor负责检查当前网络是否可用,监听默认网络状态

业务层

api

api为API集合

export namespace api {
  export const ARTICLE_HARMONY = 'harmony/index/json'
  ....
}

XxxService

XxxService根据不同场景下的接口进行区分处理,将不同场景业务下的请求以XxxService表现。 以文章类请求为例子:

import { BaseModel, httpCall } from 'network'
import { api } from '../ApiPath'

export class ArticleService {
  static getHarmonyArticle(): Promise<BaseModel<Object>> {
    return httpCall.get<Object>(api.ARTICLE_HARMONY, '', true)
  }
}

Client调用

客户端根据不同业务需求调用对应Service发起网络请获取指定数据类。

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    Column({space: 20}) {
      Text(this.message)
      Button('Click').onClick(() => {
        ArticleService.getHarmonyArticle()
      })
    }
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
  }
}

效果

最终效果

ps:文章案例仓库地址

结尾

鸿蒙笑传之RCP到这里就结束了,喜欢的xdm可以点个赞,不喜欢的白字儿们评论区吐口痰在走(你不许说他!他是我die!),不行了活不了了,下期再见,评论区扣1复活博主,爱你们捏💗💗。