项目最初只有一个页面要获取栏目列表时,直接在页面里写网络调用似乎很自然。等第二个页面要复用相同接口,第三个页面希望处理重试,问题就开始出现:每个页面分别引用三方库,错误文案、超时、数据解析规则各写一套。升级依赖时,一项看似普通的版本调整会扩散到整片业务代码。

要解决的不是“如何把包安装成功”这么简单,而是明确三方依赖应该在哪里结束,业务逻辑应该从哪里开始。这套连载以「资料目录客户端」为工程主线,从 ohpm 管理、类型隔离、网络边界,到升级回退与质量门禁,逐步建立可维护的 ArkTS 三方库接入方式。

三方库集成与依赖治理技术封面

一、先选边界,再选包

网络请求可以使用系统公开网络能力,也可以根据项目需求使用适配 HarmonyOS 的三方库。系统接口通常能让依赖更少、能力边界更清楚;使用三方库则可能方便已有封装迁移或复用统一拦截能力,但必须付出包版本、供应链、兼容性和维护成本。选型时要问三个问题:是否确实依赖该库的特性?是否有与目标 SDK 和项目类型匹配的公开说明?未来能否替换实现而不影响 UI?

此处选择社区三方包 @ohos/axios 作为网络调用示例,核心目的在于说明边界设计。华为开发者的三方件 FAQ 明确给出了该包调用 axios.post 及 AxiosResponse 等类型的示例。为使请求路径具体可理解,示例选择公开的 JSONPlaceholder 文章列表接口 https://api.jsonplaceholder.dev/posts,其响应是含数字 id 与字符串 title 的数组。本项目将数字标识转为字符串,形成自己的目录模型。公网示例服务适用于理解接口集成,正式应用应替换为自己的服务及数据协议。

对页面而言,它真正需要的是 CatalogItem[],而不是 AxiosResponse。如果页面看见了三方库的错误类型、请求配置和版本细节,它就与网络技术实现耦合了。合理的依赖方向应该是:页面 → 业务接口 → 网关适配器 → 三方库。只有最后一层直接导入 @ohos/axios。

模块边界与三方依赖方向图

这张结构图说明两个决策:其一,CatalogPort 负责稳定的业务契约,换库时页面无需跟着修改;其二,第三方仓库包由引用它的 entry 模块明确声明,不能期待根工程的依赖项自动解决所有打包场景。

二、让 ohpm 记录真实的依赖关系

HarmonyOS 工程使用 oh-package.json5 描述包依赖,ohpm 负责安装与解析。实际依赖应优先写在真正使用它的模块级文件中。官方“添加依赖项”文档明确区分 dependencies、devDependencies 和仅适用于动态 HSP 场景的 dynamicDependencies;把运行时需要的三方库放进开发依赖,容易导致产物收集与运行行为不一致。

在项目的 entry 模块目录执行:

cd entry
ohpm install @ohos/axios
ohpm list

安装会根据实际仓库信息更新模块的 oh-package.json5 和相关锁定记录。由于三方包版本随仓库维护而演进,工程中应保留安装工具解析出的真实版本,不需要手工填写一个未经核验的版本号。打开模块级配置文件,检查 dependencies 中是否出现 @ohos/axios,并保留工程模板原有的其余字段。若需复查传递依赖,可用 ohpm list -d 2 查看更深层的依赖树;ohpm list 默认只显示直接依赖,误把它当成完整供应链清单,会低估风险。团队应将经过兼容性验证的依赖配置和锁定记录一同纳入版本管理。

应用访问网络时,仍需遵守系统的权限声明。在 entry/src/main/module.json5 的 module 节点中合并所需权限,原有 abilities 等配置不应删除:

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

权限声明与依赖安装负责不同的事情:前者让应用具备请求互联网的系统条件,后者把三方库纳入构建依赖图。二者任意一个缺失,都不能靠业务层捕获异常来真正修好配置。

三、业务契约和 Axios 适配器必须分开

先定义项目内部的模型。这个模型描述一个目录项需要什么字段,而不关心响应头或具体包如何返回它。示例 API 中的数字 id 将在适配层转换为内部稳定的字符串 id:

// entry/src/main/ets/service/CatalogPort.ets
export interface CatalogItem {
  id: string;
  title: string;
}

export interface CatalogPort {
  loadItems(): Promise<CatalogItem[]>;
}

适配器统一调用第三方库,再把返回值转换为业务模型。公开示例服务返回 JSON 数组,数组元素必须包含数字 id 和非空文本 title;内部契约则使用字符串标识。把校验放在网关里,是为了避免不符合协议的数据悄悄进入 UI。页面没有责任研究服务器每种响应字段的细节。

// entry/src/main/ets/service/AxiosCatalogGateway.ets
import axios, { AxiosResponse } from '@ohos/axios';
import { CatalogItem, CatalogPort } from './CatalogPort';

interface RemotePost {
  id: number;
  title: string;
}

export function parseCatalog(data: string): CatalogItem[] {
  const records: RemotePost[] = JSON.parse(data) as RemotePost[];
  if (!Array.isArray(records)) {
    throw new Error('CATALOG_FORMAT');
  }
  const items: CatalogItem[] = [];
  for (let i: number = 0; i < records.length; i++) {
    const record: RemotePost = records[i];
    if (record === null || typeof record.id !== 'number' ||
        record.id <= 0 || typeof record.title !== 'string' ||
        record.title.length === 0) {
      throw new Error('CATALOG_FORMAT');
    }
    items.push({ id: record.id.toString(), title: record.title });
  }
  return items;
}

export class AxiosCatalogGateway implements CatalogPort {
  async loadItems(): Promise<CatalogItem[]> {
    const response: AxiosResponse = await axios.get(
      'https://api.jsonplaceholder.dev/posts', { timeout: 8000 }
    );
    return parseCatalog(JSON.stringify(response.data));
  }
}

这里 JSON.stringify 和 JSON.parse 的组合用于展示从不受信任的响应数据建立应用内部契约的过程;正式项目应结合后台协议改用更高效、明确的字段校验与解析方案,并考虑空值、非法数字、重复标识及嵌套对象。timeout: 8000 是本客户端自己规定的超时阈值,不是性能测试结果,也不能替代统一重试、取消请求与幂等性设计。

UI 页面只依赖 CatalogPort 的数据结果。如果以后切换到系统网络接口或内部 SDK,页面仍可以保留业务类型:

// entry/src/main/ets/pages/Index.ets
import { CatalogItem } from '../service/CatalogPort';
import { AxiosCatalogGateway } from '../service/AxiosCatalogGateway';

@Entry
@Component
struct Index {
  @State items: CatalogItem[] = [];
  @State message: string = '准备加载';
  private gateway: AxiosCatalogGateway = new AxiosCatalogGateway();

  aboutToAppear(): void {
    this.message = '正在获取目录';
    this.gateway.loadItems()
      .then((value: CatalogItem[]): void => {
        this.items = value;
        this.message = value.length === 0 ? '暂无目录' : '目录已加载';
      })
      .catch((error: Error): void => {
        this.message = '目录暂时不可用';
        console.error('CatalogGateway: ' + error.message);
      });
  }

  build() {
    Column({ space: 16 }) {
      Text('资料目录').fontSize(26)
      Text(this.message).fontColor('#667085')
      List() {
        ForEach(this.items, (item: CatalogItem) => {
          ListItem() { Text(item.title).padding(14) }
        }, (item: CatalogItem): string => item.id)
      }
      .width('100%')
      .layoutWeight(1)
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

这份调用代码让业务页面只处理加载、空态、错误提示和数据展示。注意示例为了展示完整接入关系,在页面中直接创建网关对象;进一步模块化时可以由应用入口或服务工厂注入 CatalogPort,从而在测试中替换成固定数据实现。这正是第二篇讨论类型封装与错误映射时需要扩展的设计点。

四、安装成功不等于接口就可靠

三方包集成至少有四种不同的失败位置。解析阶段:仓库地址有误、包名拼写不正确或版本范围不存在,ohpm install 就会失败;先检查 .ohpmrc 和网络,再考虑业务代码。构建阶段:把库放在不恰当的依赖层级,可能导致代码收集不完整;对照官方不同依赖字段的编译行为检查。请求阶段:网络、超时或 HTTP 错误需要在网关层映射成稳定错误。响应阶段:HTTP 请求有响应不代表 JSON 字段符合应用约定,数据校验仍不可省。

依赖接入与请求响应的四层排查路径

图中的红色箭头分别指向模块级生产依赖、权限声明以及响应结构校验——这三项是最常见的责任混淆点。阅读日志时最好先识别失败发生在哪一层,而不是把任何报错都归结为“axios 有问题”。配置类错误与运行时数据错误的修复方式完全不同。

还要注意两个安全边界。第一,三方依赖具有传递依赖,升级前应检查仓库来源、包维护状态、许可证和公开漏洞信息,而不只是比较版本号。第二,示例中写在源码里的 URL 只是接口地址,鉴权令牌不能硬编码到文章代码或仓库。涉及用户隐私和敏感参数时,传输与日志策略也需要独立审查。

五、为下一阶段留好替换能力

比较合理的首次接入成果不是写了多少个 axios.get,而是完成了一条能够清晰定位问题的调用链:页面只认识 CatalogItem,接口明确成功数据的形状,适配器隔离三方库,ohpm 记录模块的真实运行依赖,权限由平台配置显式声明。

这份结构适合页面数量逐渐增多、需要调整网络客户端的应用。对于仅有一个简单请求的小型工具,可以根据维护成本精简层次,但仍建议保留数据验证。下一篇将继续沿用 CatalogPort 和 AxiosCatalogGateway,把模糊的网络错误改造成业务可处理的状态,并处理加载竞争与类型边界,避免把适配器重新写成巨型工具类。

参考资料:

Logo

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

更多推荐