HarmonyOS 三方库工程化集成(连载02-06):ArkTS类型封装与接口适配【共创稿事节】
依赖版本固定以后,集成工作才真正进入容易出问题的阶段:三方库已经能安装,调用处却散落着不同的数据转换逻辑。页面 A 把 article_id 当作数字,页面 B 又把它拼成字符串;某个网络请求返回 null,列表直接读取 data.items;升级库后返回类型名称变化,所有业务页面一起报错。究其原因,应用的业务模型被上游返回格式牵着走了。
这次继续使用消息中心的文章列表作为场景。上一篇解决了 ohpm 的版本与依赖树,本篇不再重复安装命令,而是讨论如何在 ArkTS 中让三方 SDK 止步于接口边界。页面只接收稳定的 Notice,请求层处理 SDK 的配置和响应结构,适配层再把上游字段转换为业务字段。哪怕之后替换请求库,页面也不应跟着重写。

为什么 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 则属于本例后端的业务协议,两者不能互换。后端若使用不同的成功码,必须修改这一处约定而不是随意放宽所有响应检查。对大列表,转换前还应根据业务上限控制数量,避免不可预期的大响应直接进入渲染层。
这个仓库实现暂时只做一次列表读取,后续才处理超时、取消、重试与错误分类。尤其要避免把所有失败都包装成“网络不好”:字段结构变化、业务拒绝访问、服务器异常和连接失败有不同的处理路径。接口适配负责隔离类型与格式,异常治理则是下一篇要单独展开的主题。

页面只消费稳定模型
页面的责任是根据加载状态绘制内容,不应关心三方库是从网络、缓存还是本地数据得到结果。定义列表状态为 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 页面。
更多推荐
所有评论(0)