如何让鸿蒙 Axios 网络请求像诗一样优雅

1,185 阅读6分钟

时光仿若白驹过隙,转瞬即逝,距离我上次发布的有关封装网络请求库的文章 如何让 Android 网络请求像诗一样优雅 已经有一年多的时间了,随着华为纯血鸿蒙的正式使用,鸿蒙 App 的开发也提上了日程。在 Harmony 应用开发中,网络请求是必不可少的,如何封装才能使自己的网络请求代码更加简洁优雅,更具扩展性,方便以后的开发呢?本文是基于 Axios 网络请求库来做的二次封装,好了,废话不多说,开整 ~

依赖

首先,使用命令下载安装 Axios 库。

ohpm install @ohos/axios

安装完成之后,我们就可以在 oh-package.json5 中看到该依赖。

"dependencies": {
  "@ohos/axios": "^2.2.4"
}

创建实例

private axiosInstance: AxiosInstance = axios.create({
  timeout: 10000
})

定义拦截器

定义请求和响应拦截器,这里只做了请求和响应信息的日志打印,方便以后查看和调试相关问题。

this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  console.info('Axios Request:' + JSON.stringify(config))
  return config
})

this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
  console.info('Axios Response:' + JSON.stringify(response))
  return response
})

数据模型

export type NetParamType = string | number | boolean

export class BaseResult<T> {
  readonly code: number = 0
  readonly message?: string = ''
  readonly warnMessage?: string = ''
  readonly data?: T
}

GET 请求

先定义一个私有方法执行原始 get 请求。

private axiosGet<T>(url: string, axiosConfig: AxiosRequestConfig): Promise<T> {
  return this.axiosInstance.get<T, AxiosResponse<T>, null>(url, axiosConfig).then((response: AxiosResponse<T>) => {
    const data = response.data
    if (!data) {
      Promise.reject(new Error('response data is null'))
    }
    return data
  }).catch((error: Error) => {
    return Promise.reject(error)
  })
}

然后提供一个方法,用于发起 get 请求,这里统一配置 url,请求参数,请求头 token 等。

netGet<T>(url: string, params?: Map<string, NetParamType>,
  headers?: Map<string, string>): Promise<BaseResult<T>> {
  //请求参数
  const axiosParams = new Map<string, NetParamType>()
  if (params && params.size > 0) {
    params.forEach((value, key, _) => {
      axiosParams[key] = value
    })
  }
  //请求头
  const axiosHeaders = new AxiosHeaders()
  if (globalToken.length > 0) {
    axiosHeaders.set('token', globalToken)
  }
  if (headers && headers.size > 0) {
    headers.forEach((value, key, _) => {
      axiosHeaders.set(key, value)
    })
  }
  const axiosRequestConfig: AxiosRequestConfig = {
    headers: axiosHeaders,
    params: axiosParams
  }
  const axiosUrl = BASE_URL + url
  return this.axiosGet<BaseResult<T>>(axiosUrl, axiosRequestConfig)
}

POST 请求

先定义一个私有方法执行原始 post 请求。

private axiosPost<T>(url: string, params: string, axiosConfig: AxiosRequestConfig): Promise<T> {
  return this.axiosInstance.post<T, AxiosResponse<T>, string>(url, params,
    axiosConfig).then((response: AxiosResponse<T>) => {
    const data = response.data
    if (!data) {
      Promise.reject(new Error('response data is null'))
    }
    return data
  }).catch((error: Error) => {
    return Promise.reject(error)
  })
}

然后提供一个方法,用于发起 post 请求,这里统一配置 url,请求参数,请求头 token 等。

netPost<T>(url: string, params?: Map<string, NetParamType>, headers?: Map<string, string>): Promise<BaseResult<T>> {
  let formParams = ''
  if (params && params.size > 0) {
    const formArray: string[] = []
    params.forEach((value, key, _) => {
      const encodedKey = encodeURIComponent(key)
      const encodeValue = encodeURIComponent(value)
      formArray.push(`${encodedKey}=${encodeValue}`)
    })
    formParams = formArray.join('&')
  }
  const axiosHeaders = new AxiosHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
  if (globalToken.length > 0) {
    axiosHeaders.set('token', globalToken)
  }
  if (headers && headers.size > 0) {
    headers.forEach((value, key, _) => {
      axiosHeaders.set(key, value)
    })
  }
  const axiosRequestConfig: AxiosRequestConfig = {
    headers: axiosHeaders
  }
  const axiosUrl = BASE_URL + url
  return this.axiosPost<BaseResult<T>>(axiosUrl, formParams, axiosRequestConfig)
}

文件上传

upLoadFile(url: string, filePath: string, params?: Map<string, NetParamType>,
  onUploadProgress: (progress: number) => void = () => {
  }): Promise<BaseResult<object>> {
  const formData = new FormData()
  if (params && params.size > 0) {
    params.forEach((value, key, _) => {
      formData.append(key, value)
    })
  }
  const axiosHeaders = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' })
  if (globalToken.length > 0) {
    axiosHeaders.set('token', globalToken)
  }
  try {
    let file2 = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
    let stat = fs.lstatSync(filePath)
    let buf2 = new ArrayBuffer(stat.size)
    fs.readSync(file2.fd, buf2)
    fs.fsyncSync(file2.fd)
    fs.closeSync(file2.fd)
    formData.append('video', buf2)
  } catch (e) {
    console.error('read file error: ' + JSON.stringify(e))
  }
  return this.axiosInstance.post<BaseResult<object>, AxiosResponse<BaseResult<object>>, FormData>(BASE_URL + url,
    formData, {
      headers: axiosHeaders,
      context: getContext(this),
      onUploadProgress: (event: AxiosProgressEvent): void => {
        let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
        onUploadProgress(progress)
      },
    }).then((response: AxiosResponse<BaseResult<object>>) => {
    const data = response.data
    if (!data) {
      Promise.reject(new Error('response data is null'))
    }
    return data
  }).catch((error: Error) => {
    return Promise.reject(error)
  })
}

文件下载

downLoadFile(context: Context, url: string, method: string, filePath: string,
  onDownloadProgress: (progress: number) => void = () => {
  }, onDownloadResult: (result: AxiosResponse) => void = () => {
  }, onDownloadError: (errorMsg: string) => void = () => {
  }) {
  this.axiosInstance<AxiosResponse>({
    url: url,
    method: method,
    context: context,
    filePath: filePath,
    onDownloadProgress: (event: AxiosProgressEvent): void => {
      let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
      onDownloadProgress(progress)
    }
  }).then((result: AxiosResponse) => {
    onDownloadResult(result)
  }).catch((e: AxiosError) => {
    onDownloadError(e.message)
  })
}

带身份验证的请求

HTTP 异常的状态码有很多,需要统一处理的状态码主要是 401 ,表示 token 失效,需要重新刷新 token,毕竟这会直接影响我们几乎所有的网络请求。

//获取身份信息,比如 token
refreshIdentity(): Promise<BaseResult<IdentityInfo[]>> {
  return GeneralRequest.getIdentityInfo(this)
}

这里定义一个方法,用于 token 失效的时候重新请求获取 token,获取到新的 token 之后,再一次发起我们的请求。

private requestByCheckIdentity<T>(block: () => Promise<BaseResult<T>>): Promise<BaseResult<T>> {
  if (globalToken.length == 0) {
    return this.refreshIdentity().then(() => {
      return block()
    })
  }
  return block().then((result) => {
    if (result.code == 401) { 
      return this.refreshIdentity().then(() => {
        return block()
      })
    } else {
      return result
    }
  })
}

执行请求都统一调用这个方法,这样我们就可以一致避免 token 失效的情况。

getByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
  headers?: Map<string, string>): Promise<BaseResult<T>> {
  return this.requestByCheckIdentity<T>(() => this.netGet<T>(url, params, headers))
}

postByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
  headers?: Map<string, string>): Promise<BaseResult<T>> {
  return this.requestByCheckIdentity<T>(() => this.netPost<T>(url, params, headers))
}

upLoadFileByCheckIdentity(url: string, filePath: string, params?: Map<string, NetParamType>,
  onUploadProgress: (progress: number) => void = () => {
  }): Promise<BaseResult<object>> {
  return this.requestByCheckIdentity<object>(() => this.upLoadFile(url, filePath, params, onUploadProgress))
}

最后贴上请求基类的完整代码

export abstract class BaseNetRequest {
  private axiosInstance: AxiosInstance = axios.create({
    timeout: 10000
  })

  constructor() {
    this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
      console.info('Axios Request:' + JSON.stringify(config))
      return config
    })
    this.axiosInstance.interceptors.response.use((response: AxiosResponse) => {
      console.info('Axios Response:' + JSON.stringify(response))
      return response
    })
  }

  private axiosGet<T>(url: string, axiosConfig: AxiosRequestConfig): Promise<T> {
    return this.axiosInstance.get<T, AxiosResponse<T>, null>(url, axiosConfig).then((response: AxiosResponse<T>) => {
      const data = response.data
      if (!data) {
        Promise.reject(new Error('response data is null'))
      }
      return data
    }).catch((error: Error) => {
      return Promise.reject(error)
    })
  }

  //get 请求
  netGet<T>(url: string, params?: Map<string, NetParamType>,
    headers?: Map<string, string>): Promise<BaseResult<T>> {
    //请求参数
    const axiosParams = new Map<string, NetParamType>()
    if (params && params.size > 0) {
      params.forEach((value, key, _) => {
        axiosParams[key] = value
      })
    }
    //请求头
    const axiosHeaders = new AxiosHeaders()
    if (globalToken.length > 0) {
      axiosHeaders.set('token', globalToken)
    }
    if (headers && headers.size > 0) {
      headers.forEach((value, key, _) => {
        axiosHeaders.set(key, value)
      })
    }
    const axiosRequestConfig: AxiosRequestConfig = {
      headers: axiosHeaders,
      params: axiosParams
    }
    const axiosUrl = BASE_URL + url
    return this.axiosGet<BaseResult<T>>(axiosUrl, axiosRequestConfig)
  }

  private axiosPost<T>(url: string, params: string, axiosConfig: AxiosRequestConfig): Promise<T> {
    return this.axiosInstance.post<T, AxiosResponse<T>, string>(url, params,
      axiosConfig).then((response: AxiosResponse<T>) => {
      const data = response.data
      if (!data) {
        Promise.reject(new Error('response data is null'))
      }
      return data
    }).catch((error: Error) => {
      return Promise.reject(error)
    })
  }

  //post 请求
  netPost<T>(url: string, params?: Map<string, NetParamType>, headers?: Map<string, string>): Promise<BaseResult<T>> {
    let formParams = ''
    if (params && params.size > 0) {
      const formArray: string[] = []
      params.forEach((value, key, _) => {
        const encodedKey = encodeURIComponent(key)
        const encodeValue = encodeURIComponent(value)
        formArray.push(`${encodedKey}=${encodeValue}`)
      })
      formParams = formArray.join('&')
    }
    const axiosHeaders = new AxiosHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
    if (globalToken.length > 0) {
      axiosHeaders.set('token', globalToken)
    }
    if (headers && headers.size > 0) {
      headers.forEach((value, key, _) => {
        axiosHeaders.set(key, value)
      })
    }
    const axiosRequestConfig: AxiosRequestConfig = {
      headers: axiosHeaders
    }
    const axiosUrl = BASE_URL + url
    return this.axiosPost<BaseResult<T>>(axiosUrl, formParams, axiosRequestConfig)
  }

  //文件下载
  downLoadFile(context: Context, url: string, method: string, filePath: string,
    onDownloadProgress: (progress: number) => void = () => {
    }, onDownloadResult: (result: AxiosResponse) => void = () => {
    }, onDownloadError: (errorMsg: string) => void = () => {
    }) {
    this.axiosInstance<AxiosResponse>({
      url: url,
      method: method,
      context: context,
      filePath: filePath,
      onDownloadProgress: (event: AxiosProgressEvent): void => {
        let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
        onDownloadProgress(progress)
      }
    }).then((result: AxiosResponse) => {
      onDownloadResult(result)
    }).catch((e: AxiosError) => {
      onDownloadError(e.message)
    })
  }

  //文件上传
  upLoadFile(url: string, filePath: string, params?: Map<string, NetParamType>,
    onUploadProgress: (progress: number) => void = () => {
    }): Promise<BaseResult<object>> {
    const formData = new FormData()
    if (params && params.size > 0) {
      params.forEach((value, key, _) => {
        formData.append(key, value)
      })
    }
    const axiosHeaders = new AxiosHeaders({ 'Content-Type': 'multipart/form-data' })
    if (globalToken.length > 0) {
      axiosHeaders.set('token', globalToken)
    }
    try {
      let file2 = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
      let stat = fs.lstatSync(filePath)
      let buf2 = new ArrayBuffer(stat.size)
      fs.readSync(file2.fd, buf2)
      fs.fsyncSync(file2.fd)
      fs.closeSync(file2.fd)
      formData.append('video', buf2)
    } catch (e) {
      console.error('read file error: ' + JSON.stringify(e))
    }
    return this.axiosInstance.post<BaseResult<object>, AxiosResponse<BaseResult<object>>, FormData>(BASE_URL + url,
      formData, {
        headers: axiosHeaders,
        context: getContext(this),
        onUploadProgress: (event: AxiosProgressEvent): void => {
          let progress = (event && event.loaded && event.total) ? Math.ceil(event.loaded / event.total * 100) : 0
          onUploadProgress(progress)
        },
      }).then((response: AxiosResponse<BaseResult<object>>) => {
      const data = response.data
      if (!data) {
        Promise.reject(new Error('response data is null'))
      }
      return data
    }).catch((error: Error) => {
      return Promise.reject(error)
    })
  }

  //获取身份信息,比如 token
  refreshIdentity(): Promise<BaseResult<IdentityInfo[]>> {
    return GeneralRequest.getIdentityInfo(this)
  }

  //带身份验证检查的请求方法
  private requestByCheckIdentity<T>(block: () => Promise<BaseResult<T>>): Promise<BaseResult<T>> {
    if (globalToken.length == 0) {
      return this.refreshIdentity().then(() => {
        return block()
      })
    }
    return block().then((result) => {
      if (result.code == 401) { //token失效,重新刷新 token。
        return this.refreshIdentity().then(() => {
          return block()
        })
      } else {
        return result
      }
    })
  }

  // 带 token 验证的 get 请求
  getByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
    headers?: Map<string, string>): Promise<BaseResult<T>> {
    return this.requestByCheckIdentity<T>(() => this.netGet<T>(url, params, headers))
  }

  // 带 token 验证的 post 请求
  postByCheckIdentity<T>(url: string, params?: Map<string, NetParamType>,
    headers?: Map<string, string>): Promise<BaseResult<T>> {
    return this.requestByCheckIdentity<T>(() => this.netPost<T>(url, params, headers))
  }

  // 带 token 验证的文件上传
  upLoadFileByCheckIdentity(url: string, filePath: string, params?: Map<string, NetParamType>,
    onUploadProgress: (progress: number) => void = () => {
    }): Promise<BaseResult<object>> {
    return this.requestByCheckIdentity<object>(() => this.upLoadFile(url, filePath, params, onUploadProgress))
  }
}

使用案例

使用时我们只需继承这个 BaseNetRequest 即可实现各种请求。

export class NetRequest extends BaseNetRequest {
  getHttpData(params: Map<string, string>): Promise<BaseResult<GetBean[]>> {
    return this.getByCheckIdentity<GetBean[]>('/huawei/harmony/net/get/test', params)
  }

  postHttpData(params: Map<string, string>): Promise<BaseResult<PostBean[]>> {
    return this.postByCheckIdentity<PostBean[]>('/huawei/harmony/net/post/test', params)
  }

  uploadLocalFile(filePath: string, params?: Map<string, NetParamType>,
    onUploadProgress: (progress: number) => void = () => {
    }): Promise<BaseResult<object>> {
    return this.upLoadFileByCheckIdentity('/huawei/harmony/net/uploadLocalFile/test', filePath, params,
      onUploadProgress)
  }
}

export const netRequest = new NetRequest()

然后在实际界面中调用即可,如下所示:

@Entry
@Component
struct Index {

  async getHttpData() {
    try {
      const response = await netRequest.getHttpData(new Map([
        ['manufacturer', 'huawei'],
        ['system', 'harmony'],
      ]))
      const data = response.data
      if (data) {
        console.info('getHttpData: ' + JSON.stringify(data))
      } else {
        console.error('getHttpData fail: ' + JSON.stringify(response))
      }
    } catch (e) {
      console.error('getHttpData error: ' + e)
    }
  }

  async postHttpData() {
    try {
      const response = await netRequest.postHttpData(new Map([
        ['system', 'harmony']
      ]))
      if (response.code == 200) {
        console.info('postHttpData success')
      } else {
        console.error('postHttpData fail: ' + JSON.stringify(response))
      }
    } catch (e) {
      console.error('postHttpData error: ' + e)
    }
  }

  async uploadLocalFile() {
    try {
      const filePath = getContext(this).cacheDir + '/video/video.mp4'
      const response = await netRequest.uploadLocalFile(filePath, new Map([
        ['manufacturer', 'huawei'],
        ['system', 'harmony'],
      ]), (progress: number) => {
        console.info('uploadLocalFile progress: ' + progress)
      })
      if (response.code == 200) {
        console.info('uploadLocalFile success')
      } else {
        console.error('uploadLocalFile fail: ' + JSON.stringify(response))
      }
    } catch (e) {
      console.error('uploadLocalFile error: ' + e)
    }
  }

  async downLoadFile() {
    try {
      const dir = getContext(this).cacheDir + '/video'
      if (!fs.accessSync(dir)) {
        fs.mkdirSync(dir)
      }
      //具体的文件路径是:/data/app/el2/100/base/<Package_Name>/haps/entry/cache/video/video.mp4
      const downloadPath = dir + '/video.mp4'
      if (fs.accessSync(downloadPath)) {
        fs.unlinkSync(downloadPath)
      }
      netRequest.downLoadFile(getContext(this),
        'https://developer.huawei.com/consumer/cn/test.mp4', 'GET',
        downloadPath, (progress) => {
          console.info('downLoadFile progress:' + progress)
        }, (result: AxiosResponse) => {
          if (result.status == 200) {
            console.info('downLoadFile success')
          } else {
            console.error('downLoadFile result:' + result)
          }
        }, (errorMsg: string) => {
          console.error('downLoadFile errorMsg: ' + errorMsg)
        })
    } catch (e) {
      console.error('downLoadFile error: ' + e)
    }
  }

  build() {
    Column({ space: 10 }) {
      Button('get request').width(100).height(50).onClick(() => {
        this.getHttpData()
      })

      Button('post request').width(100).height(50).onClick(() => {
        this.postHttpData()
      })

      Button('downLoadFile').width(100).height(50).onClick(() => {
        this.downLoadFile()
      })

      Button('uploadFile').width(100).height(50).onClick(() => {
        this.uploadLocalFile()
      })
    }
    .height('100%')
    .width('100%')
  }
}

至此,我们就已完成了鸿蒙 Axios 的二次封装。Axios 为开发者提供了高效且便捷的工具,相信在未来的鸿蒙生态建设中,Axios 将继续发挥关键作用,进一步拓展鸿蒙开发的无限潜力。