HarmonyOS NEXT请求库封装

听说鸿蒙即将不再兼容安卓,心急如焚的我连夜翻开文档,看看究竟发生了什么大变动。俗话说,万事开头难,我看到了新的版本对开发者设置了更多的障碍。之前的FA模型已经彻底被抛弃(前端转向鸿蒙的难度骤增)。取而代之的是主打Flutter开发体验的Stage模型。对于熟悉Flutter的开发者来说,这无疑是个好消息,但对于擅长TS开发的人来说,这却成了一个不小的挑战——以往的优雅方式突然变得更艰难。API10以上的版本,所有的语法错误都将导致编译失败!

基于新的模式,新的架构,我们来做一个简单的请求库吧。顺便吐槽一下,各种开源编辑器不用,用这么垃圾的IDE,各种不好使。

起手:创建文件

新建一个专门存放工具类或者类似的目录,我们把请求库文件放在里面common/request.ets。简单写一个请求库的开头,我们设计在具体得使用中需要重新new一个请求对象,也可以把下面得导出改成直接导出单例对象。

import http from '@ohos.net.http';

export  default class HttpRequest{}

一个简单的请求库就好了,全文完!

封装核心请求函数

在我们的核心请求库中,封装了专属的请求方法。我们的设计理念是让请求方法能够有效处理网络异常和自定义异常参数,这些异常处理需要根据不同公司的定义采用不同的处理逻辑。为了确保类型检测的严谨性,我们将参数处理放在了具体的请求方法中。

假设参数已经处理完毕,请求方法支持额外设置基础URL,并默认返回JSON格式的响应。错误情况将通过固定的code字段进行判定,超时时间设定为10秒。

这种设计不仅提高了灵活性和兼容性,也确保了请求的稳定性和准确性。

  /**
   * 基础请求库
   * @param method
   * @param url
   * @param data
   */
  private async _request(method: http.RequestMethod, url: string, data?: string):Promise<T|null|undefined> {

    let httpRequest = http.createHttp();
    const res = await httpRequest.request(this.baseUrl+url, {
      method,
      header: {
        'Content-Type': 'application/json'
      },
      extraData:data,
      expectDataType: http.HttpDataType.OBJECT,
      connectTimeout: 10000,
      readTimeout: 10000,
    })
    httpRequest.destroy();
    if(res.responseCode!==200)throw new Error("网络链接失败")
    const res_data=res.result as IResponseData<T>
    if(res_data.code!==1)throw new Error(res_data.msg)
    return res_data.data
  }

这里需要注意得是,新得代码要求所有对象都有明确得类型。所以我们还需要把几个类型补充一下。

// 返回得基础对象
interface IResponseData<T>{
  code:number;
  msg:string;
  data:null|undefined|T;
}
baseUrl = ''

constructor(opts?: IOpts) {
    if (opts?.baseUrl) {
      this.baseUrl = opts.baseUrl
    }
}

处理GET请求

GET请求使用还是比较频繁得(正常公司至少区分GET和POST,部分公司只有POST),我们区分几种情况来做几个应对不同情况得方法。

无参数情况

在没有参数情况下,GET请求相对简单,只需要调用上面做得核心方法就好了。或者参数设计中作为上层API层去处理,参数已经合并到URL中了

 // 简易无参数版本
  get<T>(url:string){
    return this._request<T>(http.RequestMethod.GET,url)
  }
// 使用例子
const axios = new request();

async function testGet(a: string, b: number) {
  try {
    const data = await axios.get<ITest>(`http://127.0.0.1:3000?a=${a}&b=${b}`)
  } catch (e) {
    console.log(e.message)
  }
}

使用MAP作为参数

由于参数需要使用明确得类型,我们不能使用类似any或者Record这种比较模型得类型,第一种选择就是使用MAP或者Array作为参数使用。

// 使用MAP版本
getMap<T>(url: string, params: Map<string, string | number>) {
    if (!url.includes("?")) {
      url += "?"
    }
    params.forEach((v, k) => url += `&${k}=${encodeURIComponent(v)}`)
    return this._request<T>(http.RequestMethod.GET, url)
  }
// 使用的例子
async function testGetMap(a: string, b: number) {
  try {
    const params = new Map<string, string | number>()
    params.set("a", a)
    params.set("b", b)
    const data = await axios.getMap<ITest>("http://127.0.0.1:3000", params)
  } catch (e) {
    console.log(e.message)
  }
}

使用ARRAY作为参数

MAP有着同样思路的就是使用Array作为参数传递。数组有一个问题就是没有办法区分keyvalue,所以我们设计使用第二个数组来作为keyvalue的存储。

// 使用数组传递参数
getArray<T>(url: string, params: Array<Array<string | number>>) {
    if (!url.includes("?")) {
      url += "?"
    }
    params.forEach((v) => url += `&${v[0]}=${encodeURIComponent(v[1])}`)
    return this._request<T>(http.RequestMethod.GET, url)
  }
// 使用的例子
async function testGetArray(a: string, b: number) {
  try {
    const params = [["a", a], ["b", b]]

    const data = await axios.getArray<ITest>("http://127.0.0.1:3000", params)
  } catch (e) {
    console.log(e.message)
  }
}

使用预定参数来传递

一般情况下,我们需要传递的参数是比较短的,不会超过10个字段。我们假设字段传递的内容都是字符串或者数字之类的简单值,那么我们就可以写一个新的GET请求函数。

// 从参数中获取参数
  getArgs<T>(url: string, k1?: string, v1?: IArgs, k2?: string, v2?: IArgs, k3?: string, v3?: IArgs) {
    if (!url.includes("?")) {
      url += "?"
    }
    if (k1 && v1) {
      url += `&${k1}=${encodeURIComponent(v1)}`
    }
    if (k2 && v2) {
      url += `&${k2}=${encodeURIComponent(v2)}`
    }
    if (k3 && v3) {
      url += `&${k3}=${encodeURIComponent(v3)}`
    }
    return this._request<T>(http.RequestMethod.GET, url)
  }
// 使用的例子
async function testGetArgs(a: string, b: number) {
  try {

    const data = await axios.getArgs<ITest>("http://127.0.0.1:3000", "a", a, "b", b)
  } catch (e) {
    console.log(e.message)
  }
}

其他

以上就是一些常用的处理方式了,我们可以挑一个来使用。当然除了上面的一些内容,我们也可以自定义一些其他的使用方式,获取也有一些更好用的方式呢。

// 直接传递参数
getString<T>(url: string, ...params: string[]) {
    if (!url.includes("?")) {
      url += "?"
    }
    url += params.join("&");
    return this._request<T>(http.RequestMethod.GET, url)
  }
// 使用的例子
async function testGetString(a: string, b: number) {
  try {

    const data = await axios.getString<ITest>("http://127.0.0.1:3000", "a=" + a, "b=" + b)
  } catch (e) {
    console.log(e.message)
  }
}

处理POST请求

POST的处理其实和GET非常类似了。由于系统提供的请求方式只能传递字符串,所以我们把参数的格式化放在了公开请求函数中。

简单使用

这里的第一种使用方式就是只接受字符串。这也是比较简单的一种方式。

// 简单使用POST
  post<T>(url: string, data: string) {
    return this._request<T>(http.RequestMethod.POST, url, data)
  }
// 使用的例子
async function testPost(a: string, b: number) {
  try {

    const data = await axios.post<ITest>("http://127.0.0.1:3000", JSON.stringify({ a, b }))
  } catch (e) {
    console.log(e.message)
  }
}

OBJECT的参数形式

除了直接使用字符串之外,还可以使用对象的形式去传递。但是系统不能自动推导,所以在使用的时候要指定参数的字段和类型。

注意:如果只能使用这个形式,私有方法中的extraData字段可以直接传入Object类型,不需要在使用一次JSON去字符串化。

// 使用OBJECT
postObject<T>(url: string, data: Object) {
    return this._request<T>(http.RequestMethod.POST, url, JSON.stringify(data))
}
// 使用的例子
interface IParams {
  a: string,
  b: number
}

async function testPostObject(a: string, b: number) {
  try {
    const params: IParams = { a, b }
    const data = await axios.postObject<ITest>("http://127.0.0.1:3000", params)
  } catch (e) {
    console.log(e.message)
  }
}

FORMDATA形式

有时候我们也会遇到要使用formdata形式(head中传入content-Type:multipart/form-data的情况 ),这种情况就不能使用上面的形式了,我们要新写一个参数和使用方法。

// 参数类型
interface IFormData {
  name: string;
  contentType: 'text/plain' | 'image/png' | 'image/jpeg' | 'audio/mpeg' | 'video/mp4';
  data?: string;
  remoteFileName?: string;
  filePath?: string
}
// 公开方法
 postFormData<T>(url: string, data: IFormData[]) {
    return this._requestFormData<T>(url, data)
  }
// 核心方法
   private async _requestFormData<T>(url: string, data: IFormData[]): Promise<T | null | undefined> {

    let httpRequest = http.createHttp();
    const res = await httpRequest.request(this.baseUrl + url, {
      method: http.RequestMethod.POST,
      header: {
        'Content-Type': 'application/json'
      },
      multiFormDataList: data,
      expectDataType: http.HttpDataType.OBJECT,
      connectTimeout: 10000,
      readTimeout: 10000,
    })
    httpRequest.destroy();
    if (res.responseCode !== 200) throw new Error("网络链接失败")
    const res_data = res.result as IResponseData<T>
    if (res_data.code !== 1) throw new Error(res_data.msg)
    return res_data.data
  }
// 使用例子

文件上传

上传文件也是请求库的重要组成部分,这里我们就简单写一个上传文件的方法。

async uploadFile(url: string, file: request.File) {
    let context = getContext(this) as common.UIAbilityContext;
    request.uploadFile(context, {
      url: this.baseUrl + url,
      method: http.RequestMethod.POST,
      files: [file],
      header: new Map(),
      data: []
    }).then(task => {
      task.on("complete", () => {

      })
    }).catch((err: Error) => console.log(err.message))
  }

增加拦截函数

在情况过程中,我们经常会遇到需要在请求过程中增加额外工作的情况,我们按照以往经验先增加几个拦截的函数。

注入用户鉴权和HEADER

一般用户鉴权需要在header中增加token或者其他的什么参数。我们设计全局使用统一的请求库或者每个请求使用统一的token组装方式。

首先我们增加也给后设置token的方法。这个方法必须是所有请求都会一次性设置到的,同时在初始化的时候还可以增加一个从缓存中获取的方式,这样就把设置和缓存都放在一起了。

token = ""

  setToken(token: string) {
    this.token = token
    this.headers.set("token", token)
  }

然后我们在请求函数中增加一个统一获取header的方法。这个地方也可以获取统一的header参数。如果有特殊需要设置header,可以在请求中传入对应的header参数。

  private async _request<T>(method: http.RequestMethod, url: string, data?: string): Promise<T | null | undefined> {

    let httpRequest = http.createHttp();

    let header = this.headers
    const res = await httpRequest.request(this.baseUrl + url, {
      method,
      header: header,
      extraData: data,
      expectDataType: http.HttpDataType.OBJECT,
      connectTimeout: 10000,
      readTimeout: 10000,
    })
    httpRequest.destroy();
    if (res.responseCode !== 200) throw new Error("网络链接失败")
    const res_data = res.result as IResponseData<T>
    if (res_data.code !== 1) throw new Error(res_data.msg)
    return res_data.data
  }

这里设计使用统一的headers字段来存储默认的值,每次通过独立的函数来设置或者删除对应的值。

// 内存缓存字段和值
headers = new Map<string, string>([["Content-Type", "application/json"]])

// 设置
setHeader(name: string, value: string) {
    this.headers.set(name, value)
  }
// 删除
delHeader(name: string) {
    this.headers.delete(name)
  }

拦截登录失败

如果我们的接口要求用户必须登录,这个时候我们就需要增加一个拦截判定。假如接口返回code=429为鉴权失败需要登录,我们直接增加对应的方法。

 private async _request<T>(method: http.RequestMethod, url: string, data?: string): Promise<T | null | undefined> {

    let httpRequest = http.createHttp();

    let header = this.headers
    const res = await httpRequest.request(this.baseUrl + url, {
      method,
      header: header,
      extraData: data,
      expectDataType: http.HttpDataType.OBJECT,
      connectTimeout: 10000,
      readTimeout: 10000,
    })
    httpRequest.destroy();
    if (res.responseCode !== 200) throw new Error("网络链接失败")
    const res_data = res.result as IResponseData<T>
    // 判定是否需要登录
    if (res_data.code === 429) {
      router.pushUrl({
        url: "pages/login"
      })
      return null
    }
    if (res_data.code !== 1) throw new Error(res_data.msg)
    return res_data.data
  }

这里需要注意一下,不要重复跳转登录页。

总结

通过上面的几个例子,我们大概学习了在新的版本怎么自定义一个自己的请求库。新的请求库基本满足我们的日常使用,同时也兼顾了一些我们的扩展需求。如果有更多的需要,我们也可以在这个基础上再增加额外的函数来满足我们的需要。

那么新的鸿蒙开发模式,你们觉得好用还是不还用啊?

GITHUB


HarmonyOS NEXT请求库封装
http://guofangchao.com//archives/1717594971641
作者
疯狂紫萧
发布于
2024年06月05日
许可协议