浅谈鸿蒙应用 Http Axios 请求组件泛型封装,支持 UI 响应式更新

前言

  • 市面上找了一圈关于这个得文章,也看了官方的社区和给出的解决方案,都没讲清楚怎么优雅的实现 class 反射

需求

  • 当前在开发的应用是基于鸿蒙 API 13,具体依赖如下。
  • 众所周知,Axios 默认会根据服务端响应数据,自动解析 data,如果我们定义的数据模型全部是 interface 会导致一个问题,没法实现 UI响应式更新,那这个时候,我们就需要定义 class,并借助 @ObserverV2@Trace 来实现 UI响应式更新,同时使用 class-transformer 中的 plainToClassFormExistType 方法来实现 class 实例化
  • 如果是深层嵌套,还需借助 class-transformer 中的 Type 装饰器来实现(最好使用别名,防止跟鸿蒙自带的 @Type 装饰器冲突,鸿蒙自带的 @Type 是为了持久化模型反序列化用的)
json 复制代码
{
  "modelVersion": "5.0.1",
  "description": "Please describe the basic information.",
  "dependencies": {
    "@pura/harmony-utils": "^1.2.5",
    "@pura/harmony-dialog": "^1.0.9",
    "@ohos/axios": "^2.2.4",
    "@umeng/common": "^1.1.3",
    "@umeng/analytics": "^1.2.4",
    "@umeng/utunnel": "^1.1.3",
    "@umeng/push": "^2.0.0",
    "@umeng/apm": "^1.1.0",
    "dayjs": "^1.11.13",
    "class-transformer": "^0.5.1",
    "reflect-metadata": "^0.1.13"
  },
  "devDependencies": {
    "@ohos/hypium": "1.0.19",
    "@ohos/hamock": "1.0.0"
  },
  "dynamicDependencies": {}
}

过程

utils/HttpUtil 工具类封装

  • 首先创建 utils/HttpUtil.ets
    • 重点说明,引入了,一个持久化缓存的模型 AppPersistenceModel,这个看你们自己的需求
Typescript 复制代码
import axios, { AxiosRequestConfig, AxiosResponse } from '@ohos/axios';
import { AppUtil, ToastUtil } from '@pura/harmony-utils';
import { Config } from '../config/Config';
import { PersistenceV2 } from '@kit.ArkUI';
import { AppPersistenceModel } from '../models/AppPersistenceModel';
import { LogUtil } from './LogUtil';
import { jumpLogin } from './HelperUtil';
import { BaseResponse } from '../models/ApiResponseModel';

export interface RequestConfig<D = object> extends AxiosRequestConfig<D> {
  ignoreError?: boolean
  ignoreLoginError?: boolean
}

const parseCodeMessage = (code: number) => {
  const codeMessage: Record<number, string> = {
    400: '请求有错误',
    405: '请求URL不存在',
    500: '服务器发生错误',
    502: '网关错误',
    503: '服务不可用,服务器暂时过载或维护',
    504: '网关超时',
  }
  return codeMessage[code]
}

interface BaseResponseWithout extends Omit<BaseResponse, 'isSuccess'> {}

export class HttpUtil {
  private static request<T, D = object>(config?: RequestConfig<D>): Promise<T> {
    config = config || {}
    let url = config.url || ''
    if (!url.startsWith('http://') && !url.startsWith('https://')) {
      url = `${Config.BASE_URL}${url}`
    }
    // 连接持久化模型
    const appPersistenceModel = PersistenceV2.connect(AppPersistenceModel, () => new AppPersistenceModel())!

    // 判断是否存在 session_id
    if (appPersistenceModel.sessionId) {
      if (url.includes('?')) {
        url = `${url}&session_id=${appPersistenceModel.sessionId}`
      } else {
        url = `${url}?session_id=${appPersistenceModel.sessionId}`
      }
    }

    // 设置请求地址
    config.url = url
    config.headers = config.headers || {}
    config.headers['Content-Type'] = 'application/json'
    config.headers['X-App'] = 'ZGT_ZTRUST_APP'
    config.headers['X-App-Platform'] = 'harmony'
    config.headers['X-App-Version'] = AppUtil.getBundleInfoSync().versionName
    config.headers['X-TRACE-ID'] = appPersistenceModel.traceId
    config.headers['X-DEVICE-ID'] = appPersistenceModel.deviceUUID

    const ignoreError = config?.ignoreError
    const ignoreLoginError = config?.ignoreLoginError

    return axios.request(config).then((res: AxiosResponse<T, D>) => {
      const cookies = res.headers['set-cookie']
      let sessionId = ''
      cookies?.map((cookie: string) => {
        if (cookie.includes('PHPSESSID')) {
          sessionId = cookie.split(';')[0].split('=')[1]
        }
      })
      // 本地不存在 session_id 且服务端返回 session_id 时,将 session_id 存入本地
      if (sessionId && !appPersistenceModel.sessionId) {
        appPersistenceModel.sessionId = sessionId
      }

      // 输入日志
      LogUtil.debug(
        `[HTTP] ${config?.method} ${res.status} ${url}`,
        config?.params ? `[GET] ${JSON.stringify(config?.params)}` : '',
        config?.data ? `[POST] ${JSON.stringify(config?.data)}` : '',
        res.data as object,
      )

      // 判断状态码是否正常
      if (res.status >= 200 && res.status < 300) {
        return res
      }

      return Promise.reject({
        code: res.status,
        message: parseCodeMessage(res.status),
      })
    }).then(res => {
      if (typeof res.data === 'object') {
        const data = res.data as BaseResponseWithout
        if (typeof data.code === 'undefined' || data.code < 0) {
          return Promise.reject(res.data)
        }
      }
      return res.data
    }).catch((err: BaseResponseWithout) => {
      err.code = err.code || -1
      if (err.code > 0) {
        err.code *= -1
      }
      err.message = err.message || `请求异常,状态码:${err.code}`
      if (!ignoreError) {
        ToastUtil.showToast(err.message)
      }
      if (err.code === -1001) {
        // todo: 清除登录数据
        // 跳转登录页
        if (!ignoreLoginError) {
          jumpLogin()
        }
      }

      return Promise.resolve(err as T)
    })
  }

  static get<T>(config?: RequestConfig): Promise<T> {
    config = config || {}
    config.method = 'GET'
    return HttpUtil.request<T>(config)
  }

  static post<T, D>(config?: RequestConfig<D>): Promise<T> {
    config = config || {}
    config.method = 'POST'
    return HttpUtil.request<T, D>(config)
  }

  static put<T, D>(config?: RequestConfig<D>): Promise<T> {
    config = config || {}
    config.method = 'PUT'
    return HttpUtil.request<T, D>(config)
  }

  static delete<T, D>(config?: RequestConfig<D>): Promise<T> {
    config = config || {}
    config.method = 'DELETE'
    return HttpUtil.request<T, D>(config)
  }

  static patch<T, D>(config?: RequestConfig<D>): Promise<T> {
    config = config || {}
    config.method = 'PATCH'
    return HttpUtil.request<T, D>(config)
  }
}

config/Config 配置文件

Typescript 复制代码
export class Config {
  static readonly BASE_URL = 'https://api.xxxxxxxxxx.com/test'
}

models/AppPersistenceModel 持久化模型文件

Typescript 复制代码
@ObservedV2
export class AppPersistenceModel {
  // 隐私协议是否同意
  @Trace isAgreePrivacy: boolean = false
  // 设备跟踪ID
  @Trace traceId: string = ''
  // 设备UUID
  @Trace deviceUUID: string = ''
  // 用户会话ID
  @Trace sessionId: string = ''
  // 显示更新版本弹窗日期
  @Trace showUpdateVersionDialogDate: string = ''
}

LogUtil 工具类

  • 不是我吹牛逼,这个是真好用,没有使用 @pura/harmony-utils 提供的 LogUtil
Typescript 复制代码
import { hilog } from "@kit.PerformanceAnalysisKit"

interface Params {
  domain: number
  tag: string
  showLog: boolean
  maxLineSize: number
}

type Args = string | number | boolean | undefined | null | object

const DOMAIN = 0
const TAG = 'XXXAPP'
const SHOW_LOG = true
const FORMAT = '%{public}s'
const MAX_LINE_SIZE = 1024

const defaultParams: Params = {
  domain: DOMAIN,
  tag: TAG,
  showLog: SHOW_LOG,
  maxLineSize: MAX_LINE_SIZE,
}

export class LogUtil {
  private static domain: number = DOMAIN
  private static tag: string = TAG
  private static format: string = FORMAT
  private static showLog: boolean = SHOW_LOG
  private static maxLineSize: number = MAX_LINE_SIZE

  static init(params?: Params) {
    LogUtil.domain = params?.domain ?? defaultParams.domain
    LogUtil.tag = params?.tag ?? defaultParams.tag
    LogUtil.showLog = params?.showLog ?? defaultParams.showLog
    LogUtil.maxLineSize = params?.maxLineSize ?? defaultParams.maxLineSize
  }

  static setDomain(domain: number = DOMAIN) {
    LogUtil.domain = domain
  }

  static setTag(tag: string = TAG) {
    LogUtil.tag = tag
  }

  static setShowLog(showLog: boolean = SHOW_LOG) {
    LogUtil.showLog = showLog
  }

  static setMaxLineSize(maxLineSize: number = MAX_LINE_SIZE) {
    LogUtil.maxLineSize = maxLineSize
  }

  static debug(...args: Args[]): void {
    LogUtil.debugTag("DEFAULT", ...args)
  }

  static debugTag(tag: string, ...args: Args[]): void {
    if (!LogUtil.showLog) {
      return
    }
    LogUtil.getMessages(args, (message: string) => {
      hilog.debug(LogUtil.domain, `${LogUtil.tag}/DEBUG/${tag}`, LogUtil.format, message)
    })
  }

  static info(...args: Args[]): void {
    LogUtil.infoTag("DEFAULT", ...args)
  }

  static infoTag(tag: string, ...args: Args[]): void {
    if (!LogUtil.showLog) {
      return
    }
    LogUtil.getMessages(args, (message: string) => {
      hilog.info(LogUtil.domain, `${LogUtil.tag}/INFO/${tag}`, LogUtil.format, message)
    })
  }

  static warn(...args: Args[]): void {
    LogUtil.warnTag("DEFAULT", ...args)
  }

  static warnTag(tag: string, ...args: Args[]): void {
    if (!LogUtil.showLog) {
      return
    }
    LogUtil.getMessages(args, (message: string) => {
      hilog.warn(LogUtil.domain, `${LogUtil.tag}/WARN/${tag}`, LogUtil.format, message)
    })
  }

  static error(...args: Args[]): void {
    LogUtil.errorTag("DEFAULT", ...args)
  }

  static errorTag(tag: string, ...args: Args[]): void {
    if (!LogUtil.showLog) {
      return
    }
    LogUtil.getMessages(args, (message: string) => {
      hilog.error(LogUtil.domain, `${LogUtil.tag}/ERROR/${tag}`, LogUtil.format, message)
    })
  }

  static fatal(...args: Args[]): void {
    LogUtil.fatalTag("DEFAULT", ...args)
  }

  static fatalTag(tag: string, ...args: Args[]): void {
    if (!LogUtil.showLog) {
      return
    }
    LogUtil.getMessages(args, (message: string) => {
      hilog.fatal(LogUtil.domain, `${LogUtil.tag}/FATAL/${tag}`, LogUtil.format, message)
    })
  }

  private static getMessages(args: Args[], callback: (message: string) => void): void {
    const messages: string[] = [];
    const firstMessage =
      `┌───────${LogUtil.tag}──────────────────────────────────────────────────────────────────────────`
    const firstMessageLength = firstMessage.length
    messages.push(firstMessage)
    // 获取堆栈
    const stack = new Error().stack;
    if (stack) {
      const stackArr = stack.split('\n')
      for (let i = 0; i < stackArr.length; i++) {
        const item = stackArr[i]
        if (!item.includes(LogUtil.name)) {
          messages.push(`| ${item}`)
          break
        }
      }
      // 与第一行保持一样的长度
      messages.push(`|${'─'.repeat(firstMessageLength - 1)}`)
    }
    args.map(item => {
      let message = ""
      switch (typeof item) {
        case 'object':
          message = JSON.stringify(item, null, 2)
          break
        default:
          message = String(item)
          break
      }
      if (message.length > LogUtil.maxLineSize) {
        // 切割长字符串
        const arr = message.match(new RegExp(`.{1,${LogUtil.maxLineSize}}`, 'g'))
        if (arr) {
          arr.map((item) => {
            messages.push(`| ${item}`)
          })
        } else {
          messages.push(`| ${item}`)
        }
      } else {
        // 先按 \n 切割
        const arr = message.split('\n')
        arr.map((item) => {
          messages.push(`| ${item}`)
        })
      }
    })
    messages.push(`└${'─'.repeat(firstMessageLength - 1)}`)
    messages.map(item => {
      callback(item)
    })
  }
}

models/ApiResponseModel 模型

Typescript 复制代码
export class BaseResponse {
  code?: number
  message?: string

  isSuccess(): boolean {
    if (typeof this.code === 'string') {
      return parseInt(this.code) === 0
    }
    return this.code === 0
  }
}

export class ApiResponse<T> extends BaseResponse {
  dataset?: T

  constructor(dataset: T) {
    super()
    this.dataset = dataset
  }
}

services/HttpService.ets 请求服务

  • 目前是打算把所有的请求都放这里管理
  • 21 行很重要,那是将普通的 json 对象,反射到 class 中,并返回
  • 这样的话,可以处理自行处理并返回 objectarray
Typescript 复制代码
import { AppUtil } from "@pura/harmony-utils";
import { plainToClassFromExist } from "class-transformer";
import { ApiResponse } from "../models/ApiResponseModel";
import { UpdateVersionModel } from "../models/UpdateVersionModel";
import { HttpUtil } from "../utils/HttpUtil";

export class HttpService {
  /**
   * 获取更新版本检查
   */
  static getUpdateVersionCheck() {
    return HttpUtil.get<ApiResponse<UpdateVersionModel>>(
      {
        url: '/Mobile/AppVersion/check',
        params: {
          name: 'xxx',
          platform: 'harmony',
          version: AppUtil.getBundleInfoSync().versionName
        },
        ignoreError: true,
      },
    ).then(res => {
      return plainToClassFromExist(new ApiResponse(new UpdateVersionModel()), res)
    })
  }

  /**
   * 获取启动页广告弹窗
   */
  static getOpenAdsSwiper() {
    // return HttpUtil
  }
}

结果

  • 最终,我们就可以直接在 page 中,直接愉快的调用 class 中的 isSuccess ,以及响应式更新啦
Typescript 复制代码
fetchUpdateVersion(): void {
  HttpService.getUpdateVersionCheck()
    .then((res) => {
      if (!res.isSuccess()) {
        this.handleUpdateVersionDialogNext()
        return
      }
      const version = res.dataset!.version!
      if (this.AppPersistenceModel.showUpdateVersionDialogDate === buildUpdateVersionCache(version)) {
        // 当前版本已跳过了,判断是否今天是否已经展示过了
        this.handleUpdateVersionDialogNext()
        return
      }
      this.updateVersionModel = res.dataset!
      this.visibleUpdateVersionDialog = true
    })
}

结尾

  • 初次接触鸿蒙开发,上述如有描述不当或者错误,请及时指出,也欢迎评论区探讨或私信我进行探讨。
相关推荐
林钟雪29 分钟前
HarmonyNext实战案例:基于ArkTS的实时多人协作白板应用开发
harmonyos
轻口味2 小时前
【每日学点HarmonyOS Next知识】获取资源问题、软键盘弹起、swiper更新、C给图片设置位图、读取本地Json
c语言·json·harmonyos·harmonyosnext
林钟雪3 小时前
HarmonyNext 实战:基于 ArkTS 的高级跨设备数据同步方案
harmonyos
陈无左耳、5 小时前
HarmonyOS学习第18天:多媒体功能全解析
学习·华为·harmonyos
IT乐手6 小时前
2.6、媒体查询(mediaquery)
harmonyos
麦田里的守望者江6 小时前
Kotlin/Native 给鸿蒙使用(二)
kotlin·harmonyos
IT乐手6 小时前
2.5、栅格布局(GridRow/GridCol)
harmonyos
小时代的大玩家6 小时前
鸿蒙系统下使用AVPlay播放视频,封装播放器
harmonyos
Harmony培训部小助手6 小时前
HarmonyOS NEXT Grid 组件性能优化指南
性能优化·harmonyos
Harmony培训部小助手7 小时前
HarmonyOS NEXT 瀑布流性能优化指南
性能优化·harmonyos