HarmonyOS 工程里,依赖问题经常不是代码写错,而是版本漂移。本地刚装完能跑,换一台机器、换一次 CI、或者清掉缓存后突然构建失败。这个时候如果只改业务代码,很可能越改越乱。
这篇按 HarmonyOS 7 / API 26 工程来讲 oh-package 依赖锁定。重点是三件事:依赖版本怎么写,lock 文件怎么守住,CI 里怎么提前发现版本漂移。
版本环境先写清楚
| 项目 | 示例口径 | 说明 |
| HarmonyOS 目标 | HarmonyOS 7 / API 26 | 当前文章讨论的新工程目标 |
| 工程依赖 | oh-package.json5 | 记录直接依赖 |
| 锁定文件 | oh-package-lock.json5 | 记录解析后的确定版本 |
| CI 检查 | 安装、构建、依赖差异检查 | 防止本地和流水线不一致 |
这几项不说清楚,后面讨论依赖问题很容易变成“我这里可以”。工程协作里,“我这里可以”不是结论,CI 可复现才是结论。
直接依赖和锁定依赖不是一回事
oh-package.json5 里写的是工程想要什么,lock 文件里记录的是最终装到了什么。两者都重要。
~~~json
{
"dependencies": {
"@ohos/example-ui": "1.2.3",
"@ohos/example-utils": "^2.0.0"
},
"devDependencies": {
"@ohos/linter-rules": "0.8.0"
}
}
~~~
如果依赖写成 ^2.0.0,后面可能解析到 2.0.1、2.1.0。功能看似没变,但构建输出、类型定义、运行行为都可能变化。多人协作和 CI 里,我更倾向把核心依赖锁得更具体。
案例一:本地能跑,CI 构建失败
问题场景很典型:开发机已经有缓存,所以构建通过;CI 是干净环境,重新安装依赖后失败。
先写一个依赖检查脚本,把直接依赖和 lock 文件都纳入检查。
~~~ts
type DependencyMap = Record<string, string>
type DependencyCheckResult = {
name: string
declared: string
locked?: string
ok: boolean
reason?: string
}
function checkLockedDependencies(declared: DependencyMap, locked: DependencyMap): DependencyCheckResult[] {
return Object.keys(declared).map(name => {
const declaredVersion = declared[name]
const lockedVersion = locked[name]
if (!lockedVersion) {
return { name, declared: declaredVersion, ok: false, reason: 'missing in lock file' }
}
if (declaredVersion !== lockedVersion && !declaredVersion.startsWith('^')) {
return { name, declared: declaredVersion, locked: lockedVersion, ok: false, reason: 'version mismatch' }
}
return { name, declared: declaredVersion, locked: lockedVersion, ok: true }
})
}
~~~
这段代码不依赖具体包管理器输出,先把检查逻辑跑清楚。真正接入 CI 时,可以从 oh-package.json5 和 lock 文件里读取数据。
CI 里不要只跑构建
只跑 build 太晚了。依赖漂移应该在构建前就暴露。
~~~ts
function assertDependencyResult(results: DependencyCheckResult[]): void {
const failed = results.filter(item => !item.ok)
if (failed.length === 0) {
console.info('[dependency-check] passed')
return
}
for (const item of failed) {
console.error(
'[dependency-check] failed',
item.name,
'declared=' + item.declared,
'locked=' + (item.locked ?? 'none'),
item.reason ?? ''
)
}
throw new Error('dependency check failed')
}
~~~
CI 的目标不是把错误藏起来,而是让错误尽早、尽准地失败。依赖不一致就应该在依赖检查阶段失败,不要等到编译阶段出现一堆无关报错。
案例二:三方库升级后类型变化
第二类问题是依赖升级后 API 没报明显错误,但类型定义变了。比如之前返回 string,升级后可能返回 string | undefined。
~~~ts
type OldApiResult = {
title: string
}
type NewApiResult = {
title?: string
}
function normalizeTitle(result: NewApiResult): string {
return result.title?.trim() || '未命名内容'
}
~~~
依赖升级后,不能只看构建是否通过,还要看关键调用是否有兼容层。对业务入口多的项目,我会把三方库调用包一层 adapter。
~~~ts
class ThirdPartyAdapter {
parseTitle(result: NewApiResult): string {
return normalizeTitle(result)
}
assertRuntimeCompatible(version: string): void {
if (!version.startsWith('2.')) {
throw new Error('unsupported dependency version: ' + version)
}
}
}
~~~
这样后面库再升级,影响点集中在 adapter,不会散落到页面里。
本地验证脚本
先用假数据验证依赖检查能不能挡住问题。
~~~ts
function verifyDependencyCheck(): void {
const declared = {
'@ohos/example-ui': '1.2.3',
'@ohos/example-utils': '^2.0.0'
}
const locked = {
'@ohos/example-ui': '1.2.4',
'@ohos/example-utils': '2.1.0'
}
const result = checkLockedDependencies(declared, locked)
console.info('[verify-deps]', JSON.stringify(result))
}
~~~
预期结果是 example-ui 被拦住,因为声明 1.2.3,实际锁定 1.2.4;example-utils 因为声明了 ^2.0.0,需要看团队规则是否允许浮动。如果团队追求完全可复现,也可以把 ^ 依赖一起拦掉。
我会采用的规则
| 规则 | 原因 |
| 核心运行依赖写精确版本 | 减少线上行为变化 |
| lock 文件必须提交 | 保证团队和 CI 一致 |
| CI 构建前先查依赖 | 让版本问题提前失败 |
| 三方库调用包 adapter | 升级影响集中处理 |
| 升级依赖要有回归清单 | 避免只看能不能编译 |
回归清单
| 检查项 | 通过标准 |
| 清缓存安装 | 干净环境能安装成功 |
| 依赖锁定 | lock 文件和声明依赖一致 |
| CI 构建 | 构建失败能定位到依赖阶段 |
| 类型变化 | adapter 层能处理 undefined 等变化 |
| 关键页面 | 升级后核心页面可打开、可返回、可保存 |
小结
HarmonyOS 7 / API 26 工程里,oh-package 依赖管理不能只靠本地缓存。直接依赖、lock 文件、CI 检查和 adapter 兼容层要一起看。这样遇到“本地能跑、流水线失败”时,先查依赖锁定,而不是一上来怀疑业务代码。