依赖版本固定以后,集成工作才真正进入容易出问题的阶段:三方库已经能安装,调用处却散落着不同的数据转换逻辑。页面 A 把 article_id 当作数字,页面 B 又把它拼成字符串;某个网络请求返回 null,列表直接读取 data.items;升级库后返回类型名称变化,所有业务页面一起报错。究其原因,应用的业务模型被上游返回格式牵着走了。

这次继续使用消息中心的文章列表作为场景。上一篇解决了 ohpm 的版本与依赖树,本篇不再重复安装命令,而是讨论如何在 ArkTS 中让三方 SDK 止步于接口边界。页面只接收稳定的 Notice,请求层处理 SDK 的配置和响应结构,适配层再把上游字段转换为业务字段。哪怕之后替换请求库,页面也不应跟着重写。

SDK响应通过Adapter转换为业务模型

为什么 get<T>() 还不够

在请求代码里写 axios.get<NoticeResponse>(),可以让编辑器据此推断 response.data 的静态类型,但远端数据不会因为尖括号里的 T 就自动满足约定。接口可能在一次灰度变更后把 id 从数字改成字符串,也可能在数据异常时缺失 title。如果适配层只是 as Notice[],只是把不确定性从请求层转移到了页面层。

设计接口时可以把数据分成三种:上游 DTO 是传输格式,可能带有 article_id、headline 等服务端命名;领域模型 Notice 只保留页面真正需要的字段;错误结果采用稳定的业务分类,避免业务层判断某个 SDK 私有异常字段。这样既能保留 ArkTS 的静态约束,也能在运行时对核心字段做校验。

层次主要输入对外输出允许接触三方库
SDK 调用层URL、超时和请求参数HTTP 响应与上游 DTO是
Adapter 转换层DTO 和业务校验规则Notice 或明确异常否
Repository 仓库层业务查询条件Promise<Notice[]>通过内部调用
ArkUI 页面列表加载状态文本、图片及操作状态否

这张表还有一个容易忽略的含义:页面不必知道 article_id 的存在。只有 Adapter 认识它,外部字段重命名时才有一处改动入口。字段映射应当是一段能被单独阅读、单独复核的代码,而不是很多 ?.、?? 零散堆在组件树里。

定义边界上的两份数据结构

服务端在本例中约定返回 { code, data },其中每条数据含数字 ID、标题及更新时间。业务列表则只需要标识、标题和更新时间。字段定义采用显式 interface,必要的运行时检查集中放在转换器中。所有错误情况都映射为可搜索的错误标识,不把上游原始响应直接展示在页面。

export interface NoticeDto {
  article_id: number;
  headline: string;
  updated_at: string;
}

export interface Notice {
  id: number;
  title: string;
  updatedAt: string;
}

export class NoticeAdapter {
  static toNotice(input: NoticeDto): Notice {
    if (!Number.isInteger(input.article_id) || input.article_id <= 0) {
      throw new Error('NOTICE_ID_INVALID');
    }
    if (typeof input.headline !== 'string' || input.headline.trim().length === 0) {
      throw new Error('NOTICE_TITLE_INVALID');
    }
    if (typeof input.updated_at !== 'string' || input.updated_at.length === 0) {
      throw new Error('NOTICE_DATE_INVALID');
    }
    return {
      id: input.article_id,
      title: input.headline.trim(),
      updatedAt: input.updated_at
    };
  }
}

这段转换并未替应用决定时间时区、日期显示格式或语言本地化。updated_at 先作为业务数据保留,等接口规定了时区与字符串格式,再在独立的格式化层处理。刻意不在 Adapter 内直接执行 new Date() 的好处是:格式化策略变更时不会修改列表模型,也不容易让相同时刻在不同地区出现误读。

注意 interface 是编译阶段的结构契约,不等于远端字段的运行时校验器。上面的 Number.isInteger()、typeof 和空白检查处理了核心字段,但若接口是高安全要求的业务,还需要校验长度上限、字符集合、列表条数和嵌套结构。如果要求所有异常数据都能明确定位,建议把转换异常与字段路径一起记录到开发日志,避免输出业务正文或身份信息。

请求库应停留在仓库实现内部

上一篇锁定了 @ohos/axios 依赖。现在只允许仓库实现文件引入它,页面和 Adapter 都不直接引用请求库类型。请求成功也要检查 HTTP 状态、业务状态码和 data 数组是否存在;这三者属于不同层面的失败。下面是按本例接口约定组织的请求流程,服务地址和权限配置应与应用自己的后端保持一致。

import axios from '@ohos/axios';
import { Notice, NoticeDto, NoticeAdapter } from './NoticeAdapter';

interface NoticeEnvelope {
  code: number;
  data: NoticeDto[];
}

export class NoticeRepository {
  private baseUrl: string;
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async listNotices(): Promise<Notice[]> {
    const response = await axios.get<NoticeEnvelope>(
      `${this.baseUrl}/notices`, { timeout: 8000 }
    );
    if (response.status !== 200 || !response.data) {
      throw new Error('NOTICE_HTTP_INVALID');
    }
    if (response.data.code !== 0 || !Array.isArray(response.data.data)) {
      throw new Error('NOTICE_RESPONSE_INVALID');
    }
    return response.data.data.map((row: NoticeDto): Notice => {
      return NoticeAdapter.toNotice(row);
    });
  }
}

HTTP 200 代表传输层收到了响应,code === 0 则属于本例后端的业务协议,两者不能互换。后端若使用不同的成功码,必须修改这一处约定而不是随意放宽所有响应检查。对大列表,转换前还应根据业务上限控制数量,避免不可预期的大响应直接进入渲染层。

这个仓库实现暂时只做一次列表读取,后续才处理超时、取消、重试与错误分类。尤其要避免把所有失败都包装成“网络不好”:字段结构变化、业务拒绝访问、服务器异常和连接失败有不同的处理路径。接口适配负责隔离类型与格式,异常治理则是下一篇要单独展开的主题。

ArkTS接口模型与转换器在开发环境中的关系

页面只消费稳定模型

页面的责任是根据加载状态绘制内容,不应关心三方库是从网络、缓存还是本地数据得到结果。定义列表状态为 Notice[] 后,UI 只需要 ID、标题和更新时间;错误文案在界面边界处理。下面展示一个调用入口,网络读取放在 aboutToAppear(),结果先转换成业务模型,再赋值给组件状态。

@Component
struct NoticeList {
  @State notices: Notice[] = [];
  @State status: string = '加载中';
  private repository: NoticeRepository =
    new NoticeRepository('https://api.example.com');

  aboutToAppear(): void {
    this.loadNotices();
  }

  async loadNotices(): Promise<void> {
    this.status = '加载中';
    try {
      this.notices = await this.repository.listNotices();
      this.status = this.notices.length === 0 ? '暂无消息' : '';
    } catch (error) {
      this.status = '加载失败,请重试';
    }
  }

  build() {
    Column({ space: 12 }) {
      if (this.status.length > 0) {
        Text(this.status).fontColor('#657087');
      }
      List() {
        ForEach(this.notices, (item: Notice) => {
          ListItem() {
            Column({ space: 5 }) {
              Text(item.title).fontSize(18);
              Text(item.updatedAt).fontSize(12).fontColor('#68758A');
            }.alignItems(HorizontalAlign.Start).padding(12)
          }
        }, (item: Notice) => `${item.id}`)
      }.width('100%').layoutWeight(1)
    }.width('100%').height('100%').padding(16)
  }
}

这里的 https://api.example.com 是需要由项目替换的接口地址;提交到工程时,建议在配置层注入不同环境的服务地址,避免在 ArkUI 组件里散落字符串。若页面允许多次刷新,还应处理请求先后顺序与页面销毁后的状态更新,这些行为都属于仓库与生命周期配合的问题。

不要把测试只做成“字段能显示”

类型适配最值得检查的不是正常数据,而是错误形态。可准备一个固定数据集覆盖:合法 ID、零值或负数 ID、空标题、缺失时间字段、业务失败码、data 不是数组等情况。每个样本都应该有明确结果:转换成功并产生统一模型,或者产生可以定位的错误标识。转换器本身不依赖 ArkUI 组件,适合做轻量的独立测试。

例如,下面的输入允许稳定地检查标题裁剪与字段映射,调用结果应包含 ID 为 17、标题为“版本提醒”、原样的更新时间。更复杂的数据可通过表格逐行扩展,不需要为了覆盖每个分支都启动整个页面。

const source: NoticeDto = {
  article_id: 17,
  headline: ' 版本提醒 ',
  updated_at: '2026-11-02T09:30:00+08:00'
};
const notice: Notice = NoticeAdapter.toNotice(source);
console.info(`id=${notice.id}, title=${notice.title}`);

文章列表界面读取统一业务模型

如何识别一次接口升级的影响范围

如果三方库更新了响应包装类型,先查看仓库实现中的响应解析;如果服务端修改了字段名称,就检查 DTO 与 Adapter;如果页面交互要增加“已读”“置顶”,再修改业务模型和 UI。不要一遇到错误就从页面一路替换类型断言。回归时可以沿着“依赖版本 → 请求结果 → DTO 校验 → 模型转换 → ArkUI 渲染”的链路逐层确认变化点。

对于多模块项目,公共业务类型可以放到明确的共享模块,但不应仅为复用一个接口就制造过度依赖。生产依赖关系仍由各模块 oh-package.json5 声明;远程包、本地模块及 HAR/HSP 的引用方式应按华为开发者的依赖配置规则选择。与此相关的资料包括:添加依赖项、ohpm 依赖树、DevEco Studio 工具概述。

接口封装的价值不在于多写一个类,而在于让上游变化有固定的落点:三方库归请求实现管理,字段差异归 Adapter 处理,页面只持有业务模型。这样到了下一篇处理请求异常、重试与取消时,可以直接围绕仓库层演进,而不必重新整理所有 ArkUI 页面。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐