三方库能 ohpm install 成功,只能证明依赖被装进来了。真正升级到 HarmonyOS 7 / API 26 后,我更关心的是:它有没有偷偷改全局对象、ArkTS 编译规则有没有把旧写法卡住、版本升级后原来 12 个调用点会不会出现行为变化。

一、这次不是“库不能装”,而是“装完以后工程开始变得不可信”

这个 Demo 叫 PkgGuard。

为了把问题说清楚,我做了一个专门用于兼容性验证的测试三方包 legacy-format-kit。它不是公开仓库里某个真实品牌库,而是我把项目里常见的老式 JS 写法集中做成的一套兼容性夹具。

测试版本从:

3.4.1

升级到:

3.4.2

工程目标是 HarmonyOS 7,对应 API 26。

升级前后我固定统计几组数据:

导入位置:12
适配器 API:6
需要隔离的全局副作用:2
直接迁移产生的编译问题:3
修复后编译问题:0
API 26 回归用例:18 / 18 PASS
最终状态:READY

一开始问题并不明显。

ohpm 能解析依赖,IDE 也能跳到包源码。真正进入 ArkTS 页面以后,三个问题才开始出现:

第一,三方库入口会修改内建对象和全局注册表。
第二,旧 TypeScript 写法在 ArkTS 更严格的静态检查下暴露出类型问题。
第三,升级版本以后,原来“能跑”的 12 个调用点不代表行为仍然一致。

HarmonyOS 7 的 API 26 升级指南本身也强调,升级时除了系统 API,还要评估三方 HAR/HSP 等依赖的兼容性。对普通 JS/TS 三方库,我现在也用同样思路看待:版本升级不是依赖管理动作,而是一轮兼容性工程。

二、我最先排查的不是 API,而是 import 本身有没有副作用

ArkTS 官方文档专门提到模块副作用问题:某些三方库为了兼容老运行环境,会修改内建全局对象或者 prototype chain,这类行为会影响其他代码。

测试包的旧入口里,我故意保留了两种典型写法:

Array.prototype.legacyFirst = function () {
  return this.length > 0 ? this[0] : undefined
}

globalThis.__legacyFormatter = {
  locale: 'zh-CN'
}

如果页面直接:

import { formatPrice } from 'legacy-format-kit'

那么真正发生的并不只是“拿到 formatPrice”。

模块加载时,上面的全局改写也一起执行了。

问题在普通 Demo 里很难被发现,因为 formatPrice() 本身输出正常。直到其他页面开始依赖标准 Array 行为、测试框架做运行环境重置,或者 API 26 升级后编译器和运行时检查更严格,这种隐藏副作用才会变成很难追的异常。

所以 PkgGuard 的第一条规则是:

不允许业务页面直接 import 风险入口。

所有调用必须通过一个兼容层。

三、适配器不是“再包一层函数”,而是把危险入口挡在工程边界之外

测试包同时提供了一个不修改全局对象的纯函数入口:

legacy-format-kit/core

正式项目里如果原三方包没有这种入口,我会优先寻找它是否支持独立子模块;如果没有,就评估 fork、打补丁或迁移成内部维护库,而不是用一个 wrapper 假装已经隔离。

这段代码解决什么问题:业务侧永远只依赖 PkgGuard 自己的稳定 API,不直接依赖三方包顶层入口。

// LegacyFormatAdapter.ts
import {
  formatCurrency,
  formatDate,
  normalizeText
} from 'legacy-format-kit/core'

export class LegacyFormatAdapter {
  static formatMoney(value: number): string {
    return formatCurrency(value, 'CNY')
  }

  static formatDay(timestamp: number): string {
    return formatDate(timestamp, 'yyyy-MM-dd')
  }

  static normalize(value: string): string {
    return normalizeText(value)
  }
}

为什么这段代码有意义?

因为它做了三件事。

第一,业务页面不再知道三方库真实入口。以后替换库,只改 Adapter。
第二,ArkTS 侧得到的是明确参数和返回值,减少动态类型一路传进 UI。
第三,真正有副作用的顶层模块没有被加载,自然也不会偷偷改 Array.prototype 或 globalThis。

需要强调的是:Adapter 不能把已经发生的全局副作用“魔法隔离”。 如果你依然 import 了危险入口,副作用已经发生,再套一层类没有意义。工程上的隔离,是从依赖入口设计开始的。

四、ArkTS 的严格类型检查,反而帮我找出了 3 个以前埋着的问题

测试包旧调用方式里有这种写法:

const result = legacyFormat(input)

input 可能是 number | string | undefined,返回值也被当成 any 往下传。

在普通 TS 项目里,这种代码很容易“先跑起来再说”。ArkTS 更强调静态类型和编译期检查,升级后我把三个模糊调用点都暴露出来了。

我最后不是去关检查,而是给兼容层加了业务类型。

这段代码解决什么问题:把三方库的宽松输入收紧成应用真正允许的输入,避免 undefined 和动态对象穿进页面。

export interface MoneyInput {
  amount: number
  currency: 'CNY'
}

export class FormatFacade {
  static money(input: MoneyInput): string {
    if (!Number.isFinite(input.amount)) {
      throw new Error('amount must be finite')
    }
    return LegacyFormatAdapter.formatMoney(input.amount)
  }

  static safeText(value: string | undefined): string {
    if (!value) {
      return ''
    }
    return LegacyFormatAdapter.normalize(value)
  }
}

修完以后,原来 3 个编译问题归零。

我反而觉得这是迁移里最值钱的一步:如果升级只是想办法让旧代码“别报红”,那很容易把风险继续留到运行时。既然 API 26 升级已经把问题翻出来,不如顺手把三方库边界收紧。

五、我给三方库做了一个 Compatibility Audit,而不是靠人工全项目搜索

调用点一多,人工看 12 个文件还能接受,真实项目可能是几十上百个。

于是我做了一个很轻量的 PackageAuditService,它不尝试分析全部 JavaScript 语义,只维护这次工程关心的清单:

包版本
目标 API
允许的 import 入口
业务 Adapter 数量
已知副作用规则
回归用例

这段代码解决什么问题:把一次性的升级经验变成下一次还能重复执行的检查。

export interface PackageAuditResult {
  importCount: number
  adapterApiCount: number
  blockedSideEffects: number
  compileIssues: number
  regressionPassed: number
  regressionTotal: number
}

export class PackageAuditService {
  async audit(): Promise<PackageAuditResult> {
    const imports = await this.scanImports('legacy-format-kit')
    const sideEffects = await this.checkForbiddenEntrypoints(imports)
    const regressions = await this.runRegressionCases()

    return {
      importCount: imports.length,
      adapterApiCount: 6,
      blockedSideEffects: sideEffects,
      compileIssues: 0,
      regressionPassed: regressions.passed,
      regressionTotal: regressions.total
    }
  }
}

这里的 scanImports() 和 runRegressionCases() 都是 PkgGuard 自己的工程工具,不是 HarmonyOS 系统 API。

目的也不是造一个万能扫描器,而是把团队最容易回归的问题固定下来。

六、ohpm update 成功,只能算升级流程的第一步

ohpm 当前提供 ohpm update 来按照 semver 更新三方依赖。

我在实际项目里会把操作拆成四段:

更新依赖
↓
重新构建
↓
兼容性审计
↓
API 26 真机 / 模拟器回归

而不是看到:

ohpm update success

就提交代码。

HarmonyOS 7 / API 26 还提供 API Change Assistant,适合检查工程使用的 ArkTS / C API 行为变化。三方库不一定能被系统工具完整分析,但系统 API 变化和依赖变化往往会同时发生,所以我会把两套结果一起看。

这次 PkgGuard 从 3.4.1 升到 3.4.2 后,最终回归覆盖:

金额格式化 6 条
日期格式化 5 条
文本归一化 4 条
空值与异常 3 条
合计 18 条

全部通过才把状态改成 READY。

图二就是升级后的 DevEco Studio 现场。

左边是 PkgGuard 工程,兼容层、审计服务和结果模型分别放在 adapter / service / model;中间代码明确只从 Adapter 进入;右侧模拟器显示 12 个导入点、2 个副作用已隔离、3 个编译问题已经归零;底部 HiLog 最后一行是:

audit=PASS, result=READY

七、回归测试最容易漏的不是“正常输入”,而是老代码的容错习惯

升级三方格式化库,我最开始只看正常业务结果:

100 → ¥100.00
时间戳 → 2026-09-30

这远远不够。

旧库很多调用点其实依赖了一些不明确行为:

undefined 自动转空字符串
NaN 继续格式化
超长文本自动截断
非法日期返回 "-"

版本一升级,这些“没有写进接口但大家已经依赖”的行为最容易变化。

所以 18 条回归里,我专门留了 3 条异常和空值用例。

结果并不要求新库必须模仿旧库所有坏习惯,而是要求行为变化必须被我们主动决定。比如旧库把 undefined 格式化成 "undefined",我就直接在 Adapter 层改成空字符串,并更新业务用例。

兼容不是复刻 Bug,而是让变化变得可控。

八、运行页为什么把“已隔离副作用”和“修复后编译问题”单独显示

最终手机页显示:

HarmonyOS 7 / API 26
legacy-format-kit 3.4.2
导入位置 12
适配器 API 6
已隔离全局副作用 2
修复前编译问题 3
修复后编译问题 0
API 26 回归 18 / 18 PASS
READY

我没有只放一个绿色 PASS。

因为一个三方库升级真正要回答的是:

我们到底解决了什么风险。

如果下个版本变成:

副作用 2 → 3

即使 18 条业务用例暂时还通过,也值得重新检查。

如果编译问题重新出现,说明 ArkTS 约束或依赖入口又发生变化。

这种可解释的状态,比“昨天还能编译”更适合长期维护。

九、还有几个边界我不会在文章里假装自动解决

第一,兼容层不等于沙箱。

一个三方库只要真正执行了修改全局对象的代码,普通 Adapter 无法把整个 JS 运行时隔离开。最稳妥的是不要加载危险入口,或者修库。

第二,ArkTS 能与 JS/TS 生态交互,不代表所有 npm 包都能原样搬过来。

依赖 Node.js 专属内置模块、浏览器 DOM、动态 eval()、原型链魔改的库,都需要单独评估。HarmonyOS 运行环境和标准浏览器、Node.js 环境不是同一个东西。

第三,API 26 升级还要看依赖之外的系统行为变化。

这次文章只把三方 JS 库作为主线,不意味着系统 API 可以不回归。

第四,版本锁定要和更新策略一起设计。

核心链路上的三方库我一般不会让生产构建在无审计情况下自动漂到新版本。先在升级分支执行回归,再决定是否更新锁定版本。

十、这次留下来的不是一个“兼容脚本”,而是一条三方库进入工程的门槛

以前接三方库,我的顺序是:

安装
→ import
→ 能跑就继续

现在会变成:

看运行环境假设
→ 看模块副作用
→ 建稳定 Adapter
→ 收紧 ArkTS 类型
→ 更新依赖
→ API 26 回归
→ 再进入业务

这多出来的步骤看起来慢,后面维护反而更快。

尤其 HarmonyOS 7 继续升级以后,三方库不会永远停在今天的版本。只有把边界固定下来,下一次 ohpm update 才不会重新从全项目人工排雷。

PkgGuard 最终的 18 / 18 PASS 只是这一轮结果。

真正值得保留下来的,是这次升级之后,我们终于知道 legacy-format-kit 可以从哪里进工程、哪些入口不允许用、出了问题应该先查哪一层。

这才是我理解的“三方库适配”。

十、我还会检查“没有调用”的代码是否真的不会执行

三方库兼容还有一个容易忽略的点:有些开发者看到某个导出函数没有被调用,就认为它对应的代码一定不会影响应用。

但模块顶层语句并不等于函数调用。

只要入口模块被 import,顶层初始化、全局注册、prototype 修改就可能先执行。也就是说,下面两种风险完全不同:

危险函数存在,但从未调用

和:

危险副作用写在模块顶层,import 时已经执行

PkgGuard 的扫描器因此会把“直接导入风险入口”当成单独规则,而不是只搜索具体函数名。

这也解释了为什么我没有让业务团队用一句“我们没调用那个 API”来关闭问题。对于三方库,入口本身就是运行行为的一部分。

十一、回归用例除了结果,还要固定输入和运行环境

18 条用例全绿,如果每次输入都不一样,意义也不大。

我给 PkgGuard 固定了一组测试数据:

金额:0 / 1 / 99.9 / 1000000.01
日期:正常时间戳 / 闰日 / 0 / 非法值
文本:中文 / 英文 / emoji / 空字符串
异常:undefined / NaN / 非法日期

同一组输入分别跑升级前版本和升级后版本,再比较业务允许的结果差异。

如果差异属于预期,比如我们主动把 undefined 从 "undefined" 改成空字符串,就把新的行为写进基线;如果不是预期,就继续定位到底是三方包升级还是 Adapter 改动造成。

另外,真机和模拟器至少要保留一轮基本验证。三方 JS 包虽然主要跑在 ArkTS/JS 运行时,但一旦内部又调用系统能力、Native 插件或设备环境变量,单纯编译通过并不能覆盖全部问题。

我现在会给每次升级记录:

依赖版本
HarmonyOS / API 版本
DevEco Studio 版本
测试设备
用例基线版本

这样下次有人问“3.4.2 当时为什么能升”,不是靠记忆回答,而是能找到同一份审计结果。

十二、如果三方库确实不适合 ArkTS,我不会强行适配

工程里还有一个很现实的选择:放弃这个库。

如果一个库严重依赖 Node.js 文件系统、浏览器 DOM、大量运行时 monkey patch、动态执行字符串代码,而项目只用它 10% 的功能,那么迁移成本可能高于自己重写那部分能力。

我会算三笔账:

适配成本
后续升级成本
替代实现成本

如果 Adapter 已经开始模拟半个 Node.js 环境,那通常意味着方向不对。

三方库的价值本来是减少维护量。如果为了接它反而引入更多兼容层、polyfill 和不可解释副作用,它就失去意义了。

所以 PkgGuard 最终状态除了 READY,我还预留:

ADAPT_REQUIRED
REPLACE_RECOMMENDED
BLOCKED

不是所有包都必须以“适配成功”结束。

能明确判断某个库应该替换,同样是兼容性审计的有效结论。

参考资料

  • HarmonyOS 7 / API 26 升级适配指南
    https://developer.huawei.com/consumer/en/doc/harmonyos-releases/upgrade-adaptation
  • ArkTS 设计与 JS / TS 生态兼容说明
    https://developer.huawei.com/consumer/en/arkts/
  • ArkTS 模块副作用说明
    https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V13/arkts-module-side-effects-V13
  • ohpm update
    https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide-ohpm-update
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐