[HarmonyOS]鸿蒙如何在Axios基础上扩展rcp?

665 阅读5分钟

一、背景

鸿蒙项目立项后,为了快速对齐网络能力,我司直接基于Axios二次封装了一套基础可用的网络能力。

但随着业务不断完善,对取消网络请求的需求越发紧迫,而由于系统ohos.net.http api未提供取消相关api, 因此Axios暂未实现。

二、现状

Axios 受限于底层网络引擎,无法支持取消:

Axios 开源库 上对此问题有一些反馈(维护者推荐使用rcp,但****暂无明确适配排期 )

鸿蒙官方现状 & 未来趋势 (官方****http暂时不会支持取消 ,长期主要维护rcp网络库)

基于上述背景, 我司考虑基于Axios支持取消能力。

三、方案选型

方案一:修改axios 将ohos.net.http 改为 rcp (直接废弃http引擎)

缺点:与开源版本割裂, 可持续维护性差。

方案二 : HNet基于rcp重新开发(直接废弃axios)

缺点:业务迁移成本高。

方案三:基于axios再封装一套rcp引擎

(支持切换引擎,使用rcp引擎时支持真实取消、使用ohos引擎支持上层"伪"取消)

缺点:短期维护内部版本,长期期望统一。已给官方开源库提pr

方案三可行性分析:
  1. 基于Axios源码分析可知,axios是通过Adapter模式来适配****不同平台 , 默认ohos(即 ohos.net .http) 因此可以快速实现一个****rcp适配器 ,随时切换。

       不同平台可等价于不同的网络引擎,我们直接抹除平台概念,adapter : 对应不同网络引擎实现

  1. Axios是一套api封装, 可简单类比retrofit,扩展引擎的关键在于梳理reques、response映射关系.

request映射关系:

response 映射关系:

基于上述分析,方案三技术上完全可行

四、方案实践

4.1 工程架构

4.2 Api 设计

与现有Axios Api完全保持一致,业务可低成本切换。

1.一行代码切换引擎
let axiosInstance: HNet = HNet.createNewInstance({
  timeout: 30 * 1000,
  baseURL: Config.getApiUrl(),
  adapter: rcpAdapter, //axios 默认是"ohos" 
});

2.复用axios cancelToken 实现取消
getCommonConfig(): Promise<Response<string>> {
  let map = new Map<string, Object>();
  const source = hNet.createCancelTokenSource()
  setTimeout(() => {
    source.cancel("")
  }, 100)
  return axiosClient.get({
    url: "?_m=get_common_config",
    argParams: map,
    cancelToken: source.token,
    adapter:rcpAdapter // "rcp"
  })
}

4.3 编码

1.关键代码一:新增文件说明
新增rcp 引擎 -> rcp.js
实现request&response convert ->  convert.js
实现基本http请求(get、post、put、delete等) -> http.js
实现上传请求 -> upload.js
实现下载请求 -> download.js
2.关键代码二新增rcpAdapter
//adapter.js 文件

const knownAdapters = {
  ohos: ohosAdapter,
  rcp:rcpAdapter,
}

// defaults.js 文件中切换默认引擎,非必需
adapter: ['rcp'] // 'ohos'
3.关键代码三:rcp实现取消
let session = rcp.createSession(sessionConfig)

if (config.cancelToken) {
    config.cancelToken.promise.then(cancel => {
        if (session) {
            session.cancel();
            reject(new AxiosError('Request canceled', AxiosError.ECONNABORTED, config, null));
        }
    });
}
4.关键代码四:放开cancelToken参数
// index.d.ts
export interface AxiosRequestConfig<D = any> {
    cancelToken?: CancelToken
}
5.关键代码五:ohos上层"伪"取消
// 内部网络库会统一封装requsetError, 因此统一在此方法内处理, 伪代码

this . axiosInstance . get <T, AxiosResponse <T>, D>(url, axiosConfig) . then <( HNetResponse <T>)>( response =>  this . handleRequestSuccess (response)) . catch ( error => {  throw  this . handleRequestError (error); });
    
private handleRequestError<T = any>(err: AxiosError): Promise<HNetResponse<T> | void>  {
    if (!err) throw  new Error('Error is null');
    // error返回undefined
    if (err && (err.message == null || err.message === '' ||  err.message === 'canceled')) {
        return;
    }
    const hNetError: HNetError = {
        config: err.config as HNetRequestConfig,
        code: err.code,
        request: err.request,
        response: err.response as HNetResponse,
        isAxiosError: err.isAxiosError,
        toJSON: err.toJSON,
        status: err.status,
        cause: err.cause,
        name: err.name,
        message: err.message,
        stack: err.stack,
    };
    throw xxxError;
}

4.4 过程中遇到的一些问题

  1. js中无法正常导入官方文档中的依赖
官方文档导入姿势:import  rcp  from '@kit.RemoteCommunicationKit';

 . js中文件中导入姿势: import rcp from  '@hms.collaboration.rcp'  ; 

2. #### rcp设置 httpEventsHandler:customHttpEventsHandler, 会导致response.body 返回undifined

export interface HttpEventsHandler {
    /**
     * Callback called when a part of the HTTP response body is received.
     * If the callback is registered, then returned {@link Response|response object}
     * has undefined {@link Response.body|body} field.
     * @type {?OnDataReceive}
     * @syscap SystemCapability.Collaboration.RemoteCommunication
     * @since 4.1.0(11)
     */
    onDataReceive?: OnDataReceive;

  1. axios HttpServer 无法正常启动,可删除zlib依赖 & 更新相关依赖版本解决
// 调整后的依赖
"dependencies": {
  "body-parser": "^1.20.2",
  "cookie-parser": "~1.4.4",
  "debug": "~2.6.9",
  "element-ui": "^2.10.1",
  "express": "^4.19.2",
  "http-errors": "~1.6.3",
  "jade": "^0.29.0",
  "morgan": "~1.9.1",
  "multer": "^1.4.5-lts.1"
}

4. #### HttpServer下载有bug,实际下载成功,服务返回500 & HttpServer上传服务异常

解决手段使用免费文件测试上传和下载 -> Filebin

// upload 测试
axios.post<UploadModel, AxiosResponse<UploadModel>, FormData>('https://filebin.net/'/*this.uploadUrl*/, formData, {
  headers: { 'Content-Type': 'application/octet-stream' ,
    'accept': 'application/json',
    'bin': 'abcdefghajkmlnopqlshksakjdhsajghdjsa',
    'filename': 'bule'},
    
//download 测试
 axios<string, AxiosResponse<string>, null>({
  url: 'https://filebin.net/abcdefghajkmlnopqlshksakjdhsajghdjsa/bule'/*this.downloadUrl*/,
  method: 'get',

5. #### HttpServe无法直接测试代理,可使用**免费代理**测试

// 1.通过crul 测试 ,快速验证代理是否可用 (遇到失败的,多尝试几个)
curl --proxy http://153.101.67.170:9002 http://jsonplaceholder.typicode.com/posts

// 2.代理确认可用后,代码验证测试
axios<string, AxiosResponse<string>, null>({
  url: 'http://jsonplaceholder.typicode.com/posts',
  method: 'get',
  adapter:'rcp',
  proxy: {
    host: '153.101.67.170' ,
    port: 9002,
    exclusionList: []
  },
  connectTimeout: this.connectTimeout,
  readTimeout: this.readTimeout,
  maxBodyLength: this.maxBodyLength,
  maxContentLength: this.maxContentLength
})

  1. rcp session 需要池化,每次request结束都close 会导致性能恶化

测试业务中核心链路接口 平均耗时增加 125ms

解决方案池化 rcp session (见Hll-Axios源码 - HllRcpSessionManager.js文件) - 池化后rcp更快(110ms)

五、 使用姿势

  1. 修改并强制指定工程 axios版本号 & HNet版本号
// 备注HNet为内部网络库(基于axios封装)
"dependencies": {
    "@hll-wp/hnet": "1.0.0-alpha.3", // 内部仓库,外网不可访问
    "@ohos/axios": "1.0.0-hll.5",   // 内部仓库,外网不可访问
  }
  
   "overrides": {
    "@hll-wp/hnet": "1.0.0-alpha.3", // 内部仓库,外网不可访问
    "@ohos/axios": "1.0.0-hll.5",   // 内部仓库,外网不可访问
  }

2. ##### 初始化网络库时统一切换引擎

 // 1.使用rcp引擎 (注意ohosAdapter 底层无法真实取消 - 只是上层取消)   let  axiosInstance : HNet = HNet . createNewInstance ({  timeout : 30 * 1000 ,  baseURL : Config . getApiUrl (),  adapter : rcpAdapter });  

3. ##### 切换引擎(当前支持ohos、rcp)

axios.get<string, AxiosResponse<string>, null>(this.getUrl, {
  connectTimeout: this.connectTimeout,
  adapter: ohosAdapter
})

4. ##### 取消请求

 // 1.获取tokenSource   const source = hNet. createCancelTokenSource ()   // 2.按需求执行取消   setTimeout ( () => {  console . error ( "HNet" , "getCommonConfig()  cancel do not need cancel result" )  /** * 2.1 cancel("") 方法参数传 null、undefined、"canceled""" -> error 返回undefined * 2.2 cancel("abc") 方法参数传非2.1 内容,需要感知取消结果,Promise.catch **/
      source. cancel ( "need cancel result" ) }, 100 )   // 3.请求设置取消token  axiosInstance. get ({  url : "?_m=get_common_config" ,  argParams : map,  cancelToken : source. token  }) 

六、效果展示

七、相关资料&特别鸣谢

  1. Hll-Axios **-**扩展rcp源码(需要的可自取)
  2. axios源码
  3. rcp文档
  4. ohos.net.http文档