茶器艺科智造HarmonyOS应用实战-01-entry只为路由函数直连HAR,会不会让公共代码进两份包:审计HAP、HSP与HAR依赖边界
茶器艺科智造HarmonyOS应用实战-01-entry只为路由函数直连HAR,会不会让公共代码进两份包:审计HAP、HSP与HAR依赖边界
一个 HarmonyOS 工程同时出现 entry、libraryhsp 和 libraryhar,很容易被一句“入口依赖 HSP,HSP 再依赖 HAR”概括。可当前茶器项目并不是只有这条链:entry 既引用 libraryhsp 的主页面,也直接引用 libraryhar 的 Want 路由函数。看到这条直连后,需要分别回答两个问题:entry 是否越过了职责边界,以及同一份 HAR 会怎样进入 HAP、HSP。
静态配置已经能回答默认打包规则:当前工程没有开启 packOptions.deduplicateHar,该字段缺省为 false,同一 HAR 会分别进入依赖它的 HAP 与 HSP。真实构建与 APP 解包仍然有价值,它负责核对具体条目、摘要和体积,而不是决定规则本身。源码还能解释 entry 为什么要在页面创建前接住 Want,但仅凭两侧都 import libraryhar,不能证明 HAP 与 HSP 访问的是同一个模块级变量。本篇把这些边界逐项拆开。

本文解决四个具体问题:
- 如何从 module.json5 与 hvigorfile.ts 确认 HAP、HSP、HAR,而不是根据文件夹猜类型。
- 如何还原当前项目真实依赖图,并解释 entry 直连 HAR 的 Want 入口动机。
- 为什么普通 dependencies 不等于动态加载,以及
deduplicateHar的缺省值怎样决定重复打包。 - 如何区分配置结论、产物回读和运行时状态共享证据。
一、先按配置识别模块,不按目录名分层
当前工程根 build-profile.json5 登记了三个模块:entry、libraryhsp、libraryhar。真正决定模块身份的是各模块的 module.json5 和 Hvigor 插件,而不是 library 前缀。
把关键字段并排后,三者身份很清楚:
// entry/src/main/module.json5
{ "module": { "name": "entry", "type": "entry" } }
// libraryhsp/src/main/module.json5
{
"module": {
"name": "libraryhsp",
"type": "shared",
"deliveryWithInstall": true
}
}
// libraryhar/src/main/module.json5
{ "module": { "name": "libraryhar", "type": "har" } }
构建任务与配置相互印证:entry 使用 hapTasks,libraryhsp 使用 hspTasks,libraryhar 使用 harTasks。因此本文把它们分别称为 entry HAP、应用内 HSP 和 HAR。这里不能称 libraryhsp 为“集成态 HSP”:它的模块级 build-profile.json5 没有设置 arkOptions.integratedHsp: true,而该字段缺省为 false。deliveryWithInstall: true 只说明 HSP 随应用安装,不等于启用了集成态 HSP 产物。
| 模块 | module.json5 类型 | Hvigor 任务 | 当前主要角色 |
|---|---|---|---|
| entry | entry | hapTasks | Ability、窗口、系统入口与页面挂载 |
| libraryhsp | shared | hspTasks | 页面、组件、交互与页面状态 |
| libraryhar | har | harTasks | 模型、算法、路由协议与持久化服务 |
华为的应用程序包基础术语区分了 HAP、HAR 与 HSP:HAP 是安装和运行的基本单元,HAR 在编译期参与共享,HSP 作为动态共享包在运行时提供代码与资源。这里的“运行时共享”描述模块机制,并不自动说明当前项目采用按需下载或 dynamicDependencies。华为的集成态 HSP 指南还要求显式配置 integratedHsp: true,当前工程没有这项配置。
二、真实依赖图有三条边,而不是一条直线
三个 oh-package.json5 给出了当前工程的声明关系。entry 同时依赖 HSP 与 HAR,HSP 再依赖 HAR,HAR 自身没有本地模块依赖:
// entry/oh-package.json5
{
"dependencies": {
"libraryhsp": "file:../libraryhsp",
"libraryhar": "file:../libraryhar"
}
}
// libraryhsp/oh-package.json5
{
"dependencies": {
"libraryhar": "file:../libraryhar"
}
}
// libraryhar/oh-package.json5
{
"dependencies": {}
}
因此真实方向是:
entry HAP ───────────────→ libraryhsp HSP
│ │
└──────────────→ libraryhar HAR
↑
libraryhsp ────┘
这张图说明依赖没有反向指回 entry,也没有形成环。结合构建配置还能继续下结论:工程没有配置 packOptions.deduplicateHar,其缺省值为 false,因此 libraryhar 按默认规则会进入 entry HAP 与 libraryhsp HSP 两个产物。包内究竟有哪些条目、两份副本各占多少字节,仍要用同一源码修订生成 APP 后回读。

审计顺序不应跳步:先识别模块类型,再看构建任务,然后读依赖声明、deduplicateHar 和源码 import,最后进入产物核验。配置负责给出工具链应采用的规则,产物负责证明这次构建实际生成了什么,两者不能互相替代。
三、普通 dependencies 表示可解析依赖,不表示动态加载
当前 entry/oh-package.json5 把 libraryhsp 写在普通 dependencies 中,没有使用 dynamicDependencies。这个事实应当准确表达为:entry 在编译和运行阶段使用这个 HSP 依赖;它不是动态依赖声明。
下面两种声明不应被混为一谈:
// 当前工程采用的形式
{
"dependencies": {
"libraryhsp": "file:../libraryhsp"
}
}
// 另一种需要单独设计、配置与验证的动态依赖形式
{
"dynamicDependencies": {
"libraryhsp": "file:../libraryhsp"
}
}
华为编译构建常见问题对 dependencies 与 dynamicDependencies 作了区分。当前根产品配置启用了 useNormalizedOHMUrl: true,这与规范化模块引用有关,但仍不能被解释成“页面模块会在使用时才下载”。
更稳的描述是:
- 已确认:libraryhsp 的模块类型为 shared,并随当前应用安装;它是应用内 HSP,不是已启用的集成态 HSP。
- 已确认:entry 用普通 dependencies 引用它,没有配置 dynamicDependencies。
- 已确认:当前没有开启
deduplicateHar;按缺省规则,libraryhar 会打包到依赖它的 HAP 与 HSP 中。 - 尚未确认:本次候选 APP 中的具体条目、摘要、体积,以及两侧 import 是否获得同一个运行时模块实例。
- 未实施:按需动态依赖、HAR 去重、独立更新或跨应用共享方案。
四、entry 的主页面很薄,但 Ability 不能只剩一行装载代码
entry/src/main/ets/pages/Index.ets 从 HSP 公共入口导入 ChaqiExperiencePage,页面主体可以简化为:
import { ChaqiExperiencePage } from 'libraryhsp';
@Entry
@Component
struct Index {
build() {
ChaqiExperiencePage()
}
}
这符合“entry 挂载业务页面,HSP 承担可见交互”的分工。但 entry 还必须处理系统交给应用的生命周期对象。当前 EntryAbility 负责窗口准备、系统栏样式、首次 Want、复用实例的新 Want,以及 pages/Index 的加载。
如果把 entry 理解成只能 import HSP,那么首次 Want 到达时页面还没有创建,参数将没有稳定的交接位置。薄入口的目标是减少业务 UI,而不是禁止 Ability 调用任何底层协议。
五、entry 直连 HAR 的动机合理,但模块级 pending 不能直接当共享状态
EntryAbility.ets 直接从 HAR 导入 setChaqiRouteFromWant。源码意图是首次创建实例时由 onCreate 先保存 Want;已有实例收到新请求时由 onNewWant 更新 pending,再通过 EventHub 唤醒页面:
import { setChaqiRouteFromWant } from 'libraryhar';
onCreate(want: Want): void {
setChaqiRouteFromWant(want)
}
onNewWant(want: Want): void {
setChaqiRouteFromWant(want)
this.context.eventHub.emit('chaqiWantRoute')
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index')
}
这条 entry → HAR 依赖有明确动机:onCreate 发生在 HSP 页面建立之前,Want 必须先被接住。但当前 pending 是 HAR 文件中的模块级变量,entry HAP 负责写,libraryhsp HSP 负责读;与此同时,当前配置又会把 libraryhar 分别打进 HAP 与 HSP。静态源码只能说明两侧调用了同名 API,不能把它提升为“已经确认读写同一个 pending 实例”。
因此边界规则需要同时约束依赖方向和状态归属:
entry 可以调用 HAR 的纯解析、模型等无状态公开端口;
跨 HAP/HSP 的可变 inbox 不能只靠两侧各自 import HAR 来证明共享;
状态应放进确定唯一的宿主模块或持久化通道,或者先开启 HAR 去重并验证运行时身份;
entry 不直接修改 HSP 内部页面状态;
HAR 不得反向引用 entry 或 HSP。

图中的三条箭头与源码声明一致,表示调用方向。按当前缺省打包规则,libraryhar 会分别进入 HAP 和 HSP;图片没有表达两份副本会共享模块级状态,读者也不应从同名框推导出“同一对象”。
六、HSP 组合界面,HAR 提供可复用能力
libraryhsp/Index.ets 对外导出 ChaqiExperiencePage。这个页面负责草稿恢复、页签、茶杯参数、3D 同步、深链应用和持久化调度;多个 HSP 组件也会引用 HAR 的模型或服务。
libraryhar/Index.ets 则集中导出字体与状态模型、默认草稿、草稿存储、Want 路由、拉坯计算和切片估算等能力。可以把当前职责归纳成一份边界表:
| 层 | 应拥有 | 不应拥有 |
|---|---|---|
| entry HAP | Ability、WindowStage、系统栏、skills、备份扩展、系统 Want 入口 | 大块业务 UI、具体茶杯交互算法 |
| libraryhsp HSP | 页面、组件、ArkUI 交互、页面级状态协调 | Ability 清单、产品签名、反向调用 entry |
| libraryhar HAR | 模型、纯算法、路由协议、基于 Context 的存储服务 | 页面装饰、窗口生命周期、对 HSP 的引用 |
HAR 可以包含需要 Context 的 Preferences 服务,并不要求所有函数都是纯函数。真正要守住的是依赖方向与公开契约:底层能力不认识具体页面,上层通过公共入口使用它。
七、当前还有两个值得收口的边界缺口
第一个缺口是 entry 中仍注册了 pages/ChaqiAgentPage,该页面包含 AgentFramework 相关 UI;HSP 又有 ChaqiAgentButton。这与“主要 UI 都在 HSP”的注释存在张力。
目前静态引用中没有找到明确导航到 ChaqiAgentPage 的调用,但这不等于已经证明它不可达。Want、系统配置、历史兼容入口或运行时字符串都可能影响可达性。处理顺序应是:
- 搜索路由、Navigation、router 和 Want 目标。
- 从启动页实际触发智能体入口并记录落点。
- 核对 main_pages.json 的注册是否仍被需要。
- 再决定保留、迁入 HSP 或删除。
第二个缺口是 HAR 公共入口较宽。模型、算法、路由和存储都由一个 Index.ets 暴露,新增 import 时容易绕过预期边界。可以先增加导出清单与依赖守卫,不必立即拆成更多模块。
八、用显式策略描述允许的跨模块调用
一份轻量的边界策略就能让代码评审有共同标准:
export interface ModuleBoundaryRule {
from: 'entry' | 'libraryhsp' | 'libraryhar';
allowedTargets: string[];
allowedEntryPoints: string[];
}
export const CHAQI_BOUNDARIES: ModuleBoundaryRule[] = [
{
from: 'entry',
allowedTargets: ['libraryhsp', 'libraryhar'],
allowedEntryPoints: [
'libraryhsp:ChaqiExperiencePage',
'libraryhsp:offerChaqiRouteFromWant',
'libraryhar:pure-api-only'
]
},
{
from: 'libraryhsp',
allowedTargets: ['libraryhar'],
allowedEntryPoints: ['libraryhar:public-api']
},
{
from: 'libraryhar',
allowedTargets: [],
allowedEntryPoints: []
}
];
这不是要求运行时加载一份策略表,而是把约定变成可审查输入。示例采用“RouteInbox 由 HSP 唯一持有”的推荐方案,所以 entry 通过 HSP 的窄端口投递 Want;entry 仍可调用 HAR 的纯解析或不可变模型 API,但当前依赖模块级 pending 的 setChaqiRouteFromWant 不应直接进入允许清单。实际守卫可以扫描模块目录中的 import,只报告越界来源、目标和文件位置。
若 entry 以后还需要直接调用 HAR 的日志、迁移或启动恢复端口,应逐项登记动机和状态归属,而不是开放整个 HAR 作为入口层的随意工具箱。另一条可选路线是先完整配置 deduplicateHar: true,再用产物和双侧实例日志确认 HAP/HSP 确实访问同一份 HAR;在证据闭环前,不把它当作可靠的跨包单例方案。
九、默认重复由配置决定,具体条目和体积再由产物证明
“entry 和 HSP 都依赖 HAR”给出了消费者关系,工程的 deduplicateHar 则决定去重策略。华为工程级 build-profile.json5 文档明确说明:deduplicateHar 缺省为 false 时,HAR 会打包到每个 HAP/HSP;设置为 true 才会把重复 HAR 从 HSP 去除。当前工程没有配置该字段,所以配置结论是“默认重复打包”。
开启去重也不是只加一行布尔值。文档同时要求配置 idDefinedFilePath、保持 useNormalizedOHMUrl: true,并在 module.json5 中设置 libIsolation: false;该能力还受工具链、系统版本和运行场景限制。本文没有替当前项目启用它。
真实构建与回读仍然必不可少,但它们回答的是更具体的问题:这次候选 APP 是否遵循配置、libraryhar 的哪些代码和资源分别进入两个包、重复体积是多少、运行时状态是否确实来自同一身份。
建议保存如下核验记录:
export interface ModuleArtifactReceipt {
sourceRevision: string;
productName: string;
appSha256: string;
moduleName: string;
moduleType: 'HAP' | 'HSP';
moduleSha256: string;
harEvidence: string[];
}
export interface PackagingConclusion {
configStatus: 'DUPLICATE_EXPECTED' | 'DEDUP_REQUESTED';
artifactStatus: 'NOT_INSPECTED' | 'DUPLICATE_CONFIRMED' | 'DEDUP_CONFIRMED';
receipts: ModuleArtifactReceipt[];
reason: string;
}
对当前工程,configStatus 应写 DUPLICATE_EXPECTED,因为没有开启 deduplicateHar;在尚未构建和解包时,artifactStatus 保持 NOT_INSPECTED。只有记录了具体 APP 摘要、内部模块摘要和条目,才把产物状态写成 DUPLICATE_CONFIRMED。这样既不否认工具链的缺省规则,也不伪造本次构建证据。
华为HAR 转 HSP 指导和模块编译说明可以帮助理解编译期复用、重复打包与运行时共享的区别。目标工具链生成的实际产物用于闭环具体条目与大小;跨包单例还必须增加运行日志或可观察的实例标识,不能只看文件名。
十、最小验证矩阵:先守边界,再核包体
| 编号 | 场景 | 期望结果 | 所需证据 |
|---|---|---|---|
| A01-01 | 读取三个 module.json5 | 类型分别为 entry/shared/har | 配置快照 |
| A01-02 | 读取三个 hvigorfile.ts | 分别使用 hapTasks/hspTasks/harTasks | 构建脚本快照 |
| A01-03 | 解析模块依赖 | entry→HSP、entry→HAR、HSP→HAR,无环 | 依赖图 |
| A01-04 | 扫描 HAR import | HAR 不引用 entry/HSP | 守卫报告 |
| A01-05 | 扫描 HSP import | HSP 不引用 entry | 守卫报告 |
| A01-06 | 核对 entry 直连 HAR | 只放行纯 API;模块级 route state 标记为待迁移 | 允许清单 |
| A01-07 | 冷启动携带 Want | 页面创建后仍能取得参数 | 生命周期日志 |
| A01-08 | 主页面装配 | entry 通过 HSP 公共入口加载页面 | import 与运行画面 |
| A01-09 | 生成候选 APP | 三模块能够按目标产品装配 | 构建回执 |
| A01-10 | 解包候选 APP | 缺省配置下分别记录 HAP/HSP 中的 HAR 条目与体积 | 摘要与包内清单 |
| A01-11 | 跨包 pending 身份 | entry 写入与 HSP 读取记录相同实例标识;否则判定状态通道不成立 | 双侧运行日志 |
| A01-12 | Agent 页面入口 | 明确保留、迁移或删除依据 | 导航实录 |
本文没有执行表中的构建、解包和运行项,因此不能给出重复条目的实际体积、pending 是否跨包共享、Agent 页面运行时可达性或完整装配成功的结论。能够确认的是配置规则:deduplicateHar 缺省为 false,当前依赖图会按默认方式把 HAR 放进两个消费者产物。
十一、常见误判与排查顺序
| 现象或说法 | 先核对 | 容易混淆的原因 | 正确处理 |
|---|---|---|---|
| “目录叫 library,所以一定是 HAR” | module.json5.type | 用名称代替模块配置 | 再看 Hvigor 任务 |
| “HSP 就是按需下载” | 依赖字段与交付配置 | 把模块机制等同加载策略 | 区分 dependencies 与 dynamicDependencies |
| “entry 依赖 HAR 就越层” | 调用发生的生命周期 | 忽略页面尚未创建 | 为 Want 等系统入口保留窄端口 |
| “不解包就完全不能判断 HAR 是否重复” | deduplicateHar 及其缺省值 | 忽略工具链已定义的打包规则 | 先给配置结论,再解包核具体条目与体积 |
| “两边 import 同一 HAR,所以 pending 一定是同一对象” | 去重配置、模块隔离与双侧实例日志 | 把包名相同当成运行时身份相同 | 共享状态移到唯一宿主,或完成去重与运行验证 |
| “主页面已经在 HSP,entry 没有 UI” | main_pages.json 与页面目录 | 忽略遗留注册页 | 先验证 Agent 页面可达性 |
| “HAR 什么都能放” | 公共出口和反向 import | 把公共模块当杂物箱 | 按模型、服务、算法和协议收口 |
| “依赖无环就没有架构问题” | 允许入口与职责归属 | 只看图,不看调用语义 | 加入公开契约和可达性核验 |
排查时先看配置和调用事实,最后看产物。包体问题必须以同一次构建的 APP 为对象;拿历史 pack.info、旧输出目录或另一产品的制品比较,会得到无法追溯的结论。
十二、边界清楚的标志是每条依赖都能解释
当前茶器工程的依赖方向清楚且无环:entry 挂载 HSP 页面,HSP 使用 HAR 的模型与服务,entry 还通过 HAR 路由端口提前接住 Want。后面这条直连有具体生命周期动机,但它把模块级可变状态放在默认会重复打包的 HAR 中,因此不能仅凭源码宣布交接已经成立。
需要继续治理的是路由 inbox 的唯一宿主、公开入口宽度、entry 中 Agent 页面的归属,以及自动化依赖守卫。当前配置已经指向 HAR 默认重复打包;尚待验证的是具体副本内容、体积与跨包运行时身份。本文只读取了模块配置、依赖声明和源码 import,没有修改原工程,没有运行 hvigorw assembleHap --no-daemon,没有生成、解包或安装 APP,也没有在模拟器或真机验证页面。
更多推荐

所有评论(0)