HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十)扩展:【工程集成】主应用 + 元服务 + HSP 共享库------三模块一体化架构
摘要 :第 19 篇我们为《灵犀厨房》装上了通知系统------延时提醒 + WantAgent 回到应用。第 20 篇我们构建了一键推荐原子化服务。但这两个模块目前还各居一隅------主应用的登录页、首页、详情页在一个工程,元服务的推荐页在另一个工程。用户从元服务点击菜谱卡片,主应用需要正确接收
Want参数并直接打开RecipeDetailPage------这要求两个模块必须在同一工程、同一bundleName下运行。本篇,我们将把entry(主应用)、atomicservice(元服务)、shared(HSP 共享库)三个模块整合到一个工程中,构建标准的三模块一体化架构。严格遵循 HarmonyOS 6.1.0(API 23)规范,代码基于工程实际文件。
一、引言与系列定位
经过前 19 篇的积累,《灵犀厨房》的主应用已经非常完备。第 20 篇又新增了原子化服务模块。但要实现"元服务点击 → 主应用详情页"的闭环,两个模块必须合并到同一个工程------共享 bundleName、共享 HSP 库、共享签名。
这不仅仅是"把两个文件夹放在一起"。它涉及三个关键决策:
| 决策 | 问题 | 本篇答案 |
|---|---|---|
| 应用类型 | bundleType 用 app 还是 atomicService? |
app------因为同时包含主应用和元服务 |
| 模块类型 | atomicservice 的 module.json5 中 type 用什么? |
feature ------API 23 中 feature 模块通过 installationFree: true 提供元服务能力 |
| 代码复用 | entry 和 atomicservice 如何共享代码? | HSP 共享包(shared)------官方推荐方案,编译产物收敛为单份 |
设计决策 :为什么
bundleType: "app"而不是"atomicService"?
bundleType: "atomicService" 仅适用于整个应用只有一个独立的元服务入口 的场景。当我们同时包含主应用(entry)和元服务(atomicservice)时,应用类型是普通的 app------其中的 feature 模块通过 installationFree: true 来提供原子化能力。这是 HarmonyOS 官方文档明确规定的。
二、核心原理与底层机制深度解读
2.1 三模块架构的编译与运行模型
#mermaid-svg-PdUzcunLes1vdHD3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PdUzcunLes1vdHD3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PdUzcunLes1vdHD3 .error-icon{fill:#552222;}#mermaid-svg-PdUzcunLes1vdHD3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PdUzcunLes1vdHD3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PdUzcunLes1vdHD3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PdUzcunLes1vdHD3 .marker.cross{stroke:#333333;}#mermaid-svg-PdUzcunLes1vdHD3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PdUzcunLes1vdHD3 p{margin:0;}#mermaid-svg-PdUzcunLes1vdHD3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PdUzcunLes1vdHD3 .cluster-label text{fill:#333;}#mermaid-svg-PdUzcunLes1vdHD3 .cluster-label span{color:#333;}#mermaid-svg-PdUzcunLes1vdHD3 .cluster-label span p{background-color:transparent;}#mermaid-svg-PdUzcunLes1vdHD3 .label text,#mermaid-svg-PdUzcunLes1vdHD3 span{fill:#333;color:#333;}#mermaid-svg-PdUzcunLes1vdHD3 .node rect,#mermaid-svg-PdUzcunLes1vdHD3 .node circle,#mermaid-svg-PdUzcunLes1vdHD3 .node ellipse,#mermaid-svg-PdUzcunLes1vdHD3 .node polygon,#mermaid-svg-PdUzcunLes1vdHD3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PdUzcunLes1vdHD3 .rough-node .label text,#mermaid-svg-PdUzcunLes1vdHD3 .node .label text,#mermaid-svg-PdUzcunLes1vdHD3 .image-shape .label,#mermaid-svg-PdUzcunLes1vdHD3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PdUzcunLes1vdHD3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PdUzcunLes1vdHD3 .rough-node .label,#mermaid-svg-PdUzcunLes1vdHD3 .node .label,#mermaid-svg-PdUzcunLes1vdHD3 .image-shape .label,#mermaid-svg-PdUzcunLes1vdHD3 .icon-shape .label{text-align:center;}#mermaid-svg-PdUzcunLes1vdHD3 .node.clickable{cursor:pointer;}#mermaid-svg-PdUzcunLes1vdHD3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PdUzcunLes1vdHD3 .arrowheadPath{fill:#333333;}#mermaid-svg-PdUzcunLes1vdHD3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PdUzcunLes1vdHD3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PdUzcunLes1vdHD3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PdUzcunLes1vdHD3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PdUzcunLes1vdHD3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PdUzcunLes1vdHD3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PdUzcunLes1vdHD3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PdUzcunLes1vdHD3 .cluster text{fill:#333;}#mermaid-svg-PdUzcunLes1vdHD3 .cluster span{color:#333;}#mermaid-svg-PdUzcunLes1vdHD3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PdUzcunLes1vdHD3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PdUzcunLes1vdHD3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PdUzcunLes1vdHD3 .icon-shape,#mermaid-svg-PdUzcunLes1vdHD3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PdUzcunLes1vdHD3 .icon-shape p,#mermaid-svg-PdUzcunLes1vdHD3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PdUzcunLes1vdHD3 .icon-shape .label rect,#mermaid-svg-PdUzcunLes1vdHD3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PdUzcunLes1vdHD3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PdUzcunLes1vdHD3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PdUzcunLes1vdHD3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 📱 运行时
应用包 (Bundle)
bundleName: com.annan.lingxikitchen
entry.hap
主应用
installationFree: false
atomicservice.hap
元服务
installationFree: true
shared.hsp
共享代码
只打包一份
🔨 hvigor 编译流程
shared HSP
编译为 .hsp
entry HAP
引用 .hsp
atomicservice HAP
引用 .hsp
核心机制:
shared模块编译为.hsp(Harmony Shared Package),不独立运行,只被消费entry和atomicservice编译为.hap(Harmony Ability Package),各自独立- 三个模块共享同一个
bundleName------系统将它们视作同一个应用的不同模块 - HSP 在最终包中只存在一份副本,entry 和 atomicservice 共享引用
2.2 原子化服务的两种模块声明方式
在 HarmonyOS 中,原子化服务有两种声明方式,取决于应用的整体形态:
| 场景 | AppScope/app.json5 的 bundleType |
模块 module.json5 的 type |
installationFree |
|---|---|---|---|
| 纯元服务(整个应用只有一个入口) | atomicService |
entry |
true |
| 主应用 + 元服务(复合应用) | app |
feature |
true |
本篇选择 :
bundleType: "app"+ 模块type: "feature"+installationFree: true。因为《灵犀厨房》同时包含完整的主应用和轻量的元服务。
2.3 HSP 共享包 vs 跨模块直接 import
#mermaid-svg-SMkFiVnrNEYwdvJg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SMkFiVnrNEYwdvJg .error-icon{fill:#552222;}#mermaid-svg-SMkFiVnrNEYwdvJg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SMkFiVnrNEYwdvJg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SMkFiVnrNEYwdvJg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SMkFiVnrNEYwdvJg .marker.cross{stroke:#333333;}#mermaid-svg-SMkFiVnrNEYwdvJg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SMkFiVnrNEYwdvJg p{margin:0;}#mermaid-svg-SMkFiVnrNEYwdvJg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg .cluster-label text{fill:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg .cluster-label span{color:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg .cluster-label span p{background-color:transparent;}#mermaid-svg-SMkFiVnrNEYwdvJg .label text,#mermaid-svg-SMkFiVnrNEYwdvJg span{fill:#333;color:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg .node rect,#mermaid-svg-SMkFiVnrNEYwdvJg .node circle,#mermaid-svg-SMkFiVnrNEYwdvJg .node ellipse,#mermaid-svg-SMkFiVnrNEYwdvJg .node polygon,#mermaid-svg-SMkFiVnrNEYwdvJg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SMkFiVnrNEYwdvJg .rough-node .label text,#mermaid-svg-SMkFiVnrNEYwdvJg .node .label text,#mermaid-svg-SMkFiVnrNEYwdvJg .image-shape .label,#mermaid-svg-SMkFiVnrNEYwdvJg .icon-shape .label{text-anchor:middle;}#mermaid-svg-SMkFiVnrNEYwdvJg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SMkFiVnrNEYwdvJg .rough-node .label,#mermaid-svg-SMkFiVnrNEYwdvJg .node .label,#mermaid-svg-SMkFiVnrNEYwdvJg .image-shape .label,#mermaid-svg-SMkFiVnrNEYwdvJg .icon-shape .label{text-align:center;}#mermaid-svg-SMkFiVnrNEYwdvJg .node.clickable{cursor:pointer;}#mermaid-svg-SMkFiVnrNEYwdvJg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SMkFiVnrNEYwdvJg .arrowheadPath{fill:#333333;}#mermaid-svg-SMkFiVnrNEYwdvJg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SMkFiVnrNEYwdvJg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SMkFiVnrNEYwdvJg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SMkFiVnrNEYwdvJg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SMkFiVnrNEYwdvJg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SMkFiVnrNEYwdvJg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SMkFiVnrNEYwdvJg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SMkFiVnrNEYwdvJg .cluster text{fill:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg .cluster span{color:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SMkFiVnrNEYwdvJg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SMkFiVnrNEYwdvJg rect.text{fill:none;stroke-width:0;}#mermaid-svg-SMkFiVnrNEYwdvJg .icon-shape,#mermaid-svg-SMkFiVnrNEYwdvJg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SMkFiVnrNEYwdvJg .icon-shape p,#mermaid-svg-SMkFiVnrNEYwdvJg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SMkFiVnrNEYwdvJg .icon-shape .label rect,#mermaid-svg-SMkFiVnrNEYwdvJg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SMkFiVnrNEYwdvJg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SMkFiVnrNEYwdvJg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SMkFiVnrNEYwdvJg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 升级为
✅ HSP 共享包
shared
Index.ets 导出
import from 'shared'
路径简洁稳定
单份编译产物
❌ 跨模块直接 import
entry
import ../../atomicservice/...
路径不稳定
编译可能报错
两份代码副本
| 维度 | 跨模块直接 import | HSP 共享包 |
|---|---|---|
| 路径稳定性 | 依赖文件系统相对路径 | 通过 oh-package.json5 声明依赖 |
| 编译产物 | 两份副本(各自打包) | 单份共享引用 |
| 官方推荐度 | 非标准用法 | ✅ 官方推荐方案 |
三、架构设计 / 核心逻辑图解
3.1 三模块分层架构
#mermaid-svg-797nQrTyuaQ1qUge{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-797nQrTyuaQ1qUge .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-797nQrTyuaQ1qUge .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-797nQrTyuaQ1qUge .error-icon{fill:#552222;}#mermaid-svg-797nQrTyuaQ1qUge .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-797nQrTyuaQ1qUge .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-797nQrTyuaQ1qUge .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-797nQrTyuaQ1qUge .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-797nQrTyuaQ1qUge .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-797nQrTyuaQ1qUge .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-797nQrTyuaQ1qUge .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-797nQrTyuaQ1qUge .marker{fill:#333333;stroke:#333333;}#mermaid-svg-797nQrTyuaQ1qUge .marker.cross{stroke:#333333;}#mermaid-svg-797nQrTyuaQ1qUge svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-797nQrTyuaQ1qUge p{margin:0;}#mermaid-svg-797nQrTyuaQ1qUge .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-797nQrTyuaQ1qUge .cluster-label text{fill:#333;}#mermaid-svg-797nQrTyuaQ1qUge .cluster-label span{color:#333;}#mermaid-svg-797nQrTyuaQ1qUge .cluster-label span p{background-color:transparent;}#mermaid-svg-797nQrTyuaQ1qUge .label text,#mermaid-svg-797nQrTyuaQ1qUge span{fill:#333;color:#333;}#mermaid-svg-797nQrTyuaQ1qUge .node rect,#mermaid-svg-797nQrTyuaQ1qUge .node circle,#mermaid-svg-797nQrTyuaQ1qUge .node ellipse,#mermaid-svg-797nQrTyuaQ1qUge .node polygon,#mermaid-svg-797nQrTyuaQ1qUge .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-797nQrTyuaQ1qUge .rough-node .label text,#mermaid-svg-797nQrTyuaQ1qUge .node .label text,#mermaid-svg-797nQrTyuaQ1qUge .image-shape .label,#mermaid-svg-797nQrTyuaQ1qUge .icon-shape .label{text-anchor:middle;}#mermaid-svg-797nQrTyuaQ1qUge .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-797nQrTyuaQ1qUge .rough-node .label,#mermaid-svg-797nQrTyuaQ1qUge .node .label,#mermaid-svg-797nQrTyuaQ1qUge .image-shape .label,#mermaid-svg-797nQrTyuaQ1qUge .icon-shape .label{text-align:center;}#mermaid-svg-797nQrTyuaQ1qUge .node.clickable{cursor:pointer;}#mermaid-svg-797nQrTyuaQ1qUge .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-797nQrTyuaQ1qUge .arrowheadPath{fill:#333333;}#mermaid-svg-797nQrTyuaQ1qUge .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-797nQrTyuaQ1qUge .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-797nQrTyuaQ1qUge .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-797nQrTyuaQ1qUge .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-797nQrTyuaQ1qUge .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-797nQrTyuaQ1qUge .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-797nQrTyuaQ1qUge .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-797nQrTyuaQ1qUge .cluster text{fill:#333;}#mermaid-svg-797nQrTyuaQ1qUge .cluster span{color:#333;}#mermaid-svg-797nQrTyuaQ1qUge div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-797nQrTyuaQ1qUge .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-797nQrTyuaQ1qUge rect.text{fill:none;stroke-width:0;}#mermaid-svg-797nQrTyuaQ1qUge .icon-shape,#mermaid-svg-797nQrTyuaQ1qUge .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-797nQrTyuaQ1qUge .icon-shape p,#mermaid-svg-797nQrTyuaQ1qUge .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-797nQrTyuaQ1qUge .icon-shape .label rect,#mermaid-svg-797nQrTyuaQ1qUge .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-797nQrTyuaQ1qUge .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-797nQrTyuaQ1qUge .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-797nQrTyuaQ1qUge :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ♻️ shared 模块(HSP 共享包)
⚛️ atomicservice 模块(元服务)
📱 entry 模块(主应用)
import from 'shared'
import from 'shared'
import from 'shared'
import from 'shared'
import from 'shared'
🔧 配置层
AppScope/app.json5
bundleType: app
build-profile.json5
modules: entry, shared, atomicservice
EntryAbility
路由决策 + RecipeBridge 写入
pages/
LoginPage / Index / RecipeDetailPage / ...
business/
RecipeManager / ShoppingList / ...
services/
NotificationHelper
AtomicserviceAbility
极简初始化
pages/
Index(一键推荐)
business/RecommendEngine
推荐引擎
foundation/model/
Recipe / UserPreference / MockData
utils/RecipeBridge
★ 跨模块参数桥梁
设计原则:
- HSP 集中共享 :
RecommendEngine、Recipe模型、MockData、RecipeBridge均位于shared - UI 轻量化:元服务模块只放 UI 代码(1 个 Ability + 1 个页面),不含业务逻辑
- 参数桥梁内置 :
RecipeBridge在 shared 模块中,EntryAbility 写入,RecipeDetailPage 读取 - 独立入口 :元服务有自己的
AtomicserviceAbility,不依赖EntryAbility
3.2 元服务 → 主应用跳转完整时序
📄 RecipeDetailPage 🔗 RecipeBridge 📱 EntryAbility ⚙️ 系统框架 ⚛️ atomicservice 👤 用户 📄 RecipeDetailPage 🔗 RecipeBridge 📱 EntryAbility ⚙️ 系统框架 ⚛️ atomicservice 👤 用户 #mermaid-svg-zTUXZyJOe1tRZEuM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zTUXZyJOe1tRZEuM .error-icon{fill:#552222;}#mermaid-svg-zTUXZyJOe1tRZEuM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zTUXZyJOe1tRZEuM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zTUXZyJOe1tRZEuM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zTUXZyJOe1tRZEuM .marker.cross{stroke:#333333;}#mermaid-svg-zTUXZyJOe1tRZEuM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zTUXZyJOe1tRZEuM p{margin:0;}#mermaid-svg-zTUXZyJOe1tRZEuM .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zTUXZyJOe1tRZEuM text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zTUXZyJOe1tRZEuM .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zTUXZyJOe1tRZEuM .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zTUXZyJOe1tRZEuM #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zTUXZyJOe1tRZEuM .sequenceNumber{fill:white;}#mermaid-svg-zTUXZyJOe1tRZEuM #sequencenumber{fill:#333;}#mermaid-svg-zTUXZyJOe1tRZEuM #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zTUXZyJOe1tRZEuM .messageText{fill:#333;stroke:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zTUXZyJOe1tRZEuM .labelText,#mermaid-svg-zTUXZyJOe1tRZEuM .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .loopText,#mermaid-svg-zTUXZyJOe1tRZEuM .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zTUXZyJOe1tRZEuM .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zTUXZyJOe1tRZEuM .noteText,#mermaid-svg-zTUXZyJOe1tRZEuM .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zTUXZyJOe1tRZEuM .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zTUXZyJOe1tRZEuM .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zTUXZyJOe1tRZEuM .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zTUXZyJOe1tRZEuM .actorPopupMenu{position:absolute;}#mermaid-svg-zTUXZyJOe1tRZEuM .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zTUXZyJOe1tRZEuM .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zTUXZyJOe1tRZEuM .actor-man circle,#mermaid-svg-zTUXZyJOe1tRZEuM line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zTUXZyJOe1tRZEuM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 点击菜谱卡片 startAbility(Want{moduleName:'entry', abilityName:'EntryAbility', params:{recipeId:7,...}}) onCreate(want) 或 onNewWant(want) extractRecipeParams(want) RecipeBridge.set(id, name, ingredients) loadContent('pages/RecipeDetailPage') 加载页面 aboutToAppear() getParams() → undefined RecipeBridge.hasPending() → true {recipeId, recipeName, recipeIngredients} RecipeManager.getRecipeById(7) → 虾仁蒸蛋 显示完整菜谱详情
四、实战:三模块工程搭建
前置说明 :本章描述的是项目当前的最终状态,配置与代码均来自工程实际文件。如果你在搭建过程中遇到问题,请参考第 20.5 篇《排错指南》中记录的五个陷阱及修复方案。
Step 1:工程级配置 --- build-profile.json5 与 AppScope/app.json5
项目根 build-profile.json5 --- 声明三个模块:
json5
{
"app": {
"signingConfigs": [],
"products": [{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}]
},
"modules": [
{ "name": "entry", "srcPath": "./entry",
"targets": [{ "name": "default", "applyToProducts": ["default"] }] },
{ "name": "shared", "srcPath": "./shared",
"targets": [{ "name": "default", "applyToProducts": ["default"] }] },
{ "name": "atomicservice", "srcPath": "./atomicservice",
"targets": [{ "name": "default", "applyToProducts": ["default"] }] }
]
}
核心点解读 :
modules数组中三个模块的顺序决定了编译顺序------shared 优先编译(HSP 需先产出),entry 和 atomicservice 后编译(依赖 shared)。
AppScope/app.json5 --- 应用级配置:
json5
{
"app": {
"bundleName": "com.annan.lingxikitchen",
"vendor": "annan",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:layered_image",
"label": "$string:app_name",
"bundleType": "app"
}
}
核心点解读 :
bundleType: "app"是关键------因为工程同时包含主应用和元服务,不能设为"atomicService"。bundleName: "com.annan.lingxikitchen"是三个模块的共享标识,元服务通过它调用startAbility拉起主应用。
Step 2:shared 模块 --- HSP 共享包
shared/oh-package.json5:
json5
{
"name": "shared",
"version": "1.0.0",
"description": "共享库",
"main": "Index.ets",
"dependencies": {}
}
shared/Index.ets --- 统一导出入口:
typescript
// entry 和 atomicservice 通过 import from 'shared' 访问此处的导出
export { RecommendEngine, recommendEngine } from './src/main/ets/business/RecommendEngine';
export { Recipe, IngredientItem, StepDetail } from './src/main/ets/foundation/model/Recipe';
export { UserPreference, Gender, ActivityLevel, UserHealthProfile,
defaultPreference, defaultHealthProfile } from './src/main/ets/foundation/model/UserPreference';
export { allRecipes } from './src/main/ets/foundation/model/MockData';
export { RecipeBridge } from './src/main/ets/utils/RecipeBridge';
shared 内部目录结构:
shared/src/main/ets/
├── business/
│ └── RecommendEngine.ets ← 推荐引擎(多维度加权评分 + 去重)
├── foundation/model/
│ ├── Recipe.ets ← 菜谱数据模型
│ ├── MockData.ets ← 10 道全量菜谱模拟数据
│ └── UserPreference.ets ← 用户偏好 + 健康档案模型
└── utils/
└── RecipeBridge.ets ← ★ 跨模块参数桥梁(EntryAbility ↔ RecipeDetailPage)
核心点解读 :
Index.ets是 HSP 模块的"面子"------它决定了哪些内容对外可见。shared内部的其他文件不对消费者暴露,实现良好的封装。entry 和 atomicservice 只需import { recommendEngine } from 'shared'即可使用推荐能力。
Step 3:entry 模块 --- 主应用配置
entry/oh-package.json5 --- 声明 shared 依赖:
json5
{
"name": "entry",
"version": "1.0.0",
"description": "主应用",
"main": "Index.ets",
"dependencies": {
"shared": "file:../shared"
}
}
entry/src/main/module.json5 --- 主应用模块声明(关键配置节选):
json5
{
"module": {
"name": "entry",
"type": "entry",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet", "2in1", "wearable"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
},
// ★ 元服务跳转 deep link URI scheme
{
"actions": ["ohos.want.action.viewData"],
"entities": ["entity.system.default"],
"uris": [{ "scheme": "lingxikitchen", "host": "recipe" }]
}
]
}]
}
}
核心点解读:
type: "entry"--- 这是主应用模块,正常安装到桌面installationFree: false--- 不支持免安装(必须下载完整应用)- 第二个
skills---ohos.want.action.viewData+lingxikitchen://recipeURI scheme,为元服务的startAbility提供精确路由匹配
Step 4:atomicservice 模块 --- 元服务配置
atomicservice/oh-package.json5 --- 声明 shared 依赖:
json5
{
"name": "atomicservice",
"version": "1.0.0",
"description": "元服务",
"main": "Index.ets",
"dependencies": {
"shared": "file:../shared"
}
}
atomicservice/src/main/module.json5 --- 元服务模块声明:
json5
{
"module": {
"name": "atomicservice",
"type": "feature",
"mainElement": "AtomicserviceAbility",
"deviceTypes": ["phone", "tablet", "2in1", "wearable"],
"deliveryWithInstall": true,
"installationFree": true,
"pages": "$profile:main_pages",
"abilities": [{
"name": "AtomicserviceAbility",
"srcEntry": "./ets/atomicserviceability/AtomicserviceAbility.ets",
"exported": true,
"launchType": "singleton",
"skills": [{
"actions": ["ohos.want.action.home"],
"entities": ["entity.system.home"],
"uris": [{ "scheme": "lingxikitchen", "host": "atomicservice" }]
}]
}]
}
}
核心点解读:
type: "feature"--- 在 API 23 中,与主应用共存的元服务模块应声明为feature类型,而非atomicServiceinstallationFree: true--- 核心标志,告诉系统此模块可免安装运行launchType: "singleton"--- 元服务只有一个推荐页,单例模式避免重复创建uris---lingxikitchen://atomicservice为全局搜索提供索引标识
Step 5:atomicservice 页面 --- 一键推荐 UI + Want 跳转
typescript
// atomicservice/src/main/ets/pages/Index.ets
import { common, Want } from '@kit.AbilityKit';
import { Recipe, recommendEngine, UserPreference } from 'shared';
@Entry
@ComponentV2
struct AtomicRecommendPage {
@Local recommendations: Recipe[] = [];
@Local isLoading: boolean = true;
aboutToAppear(): void {
this.loadRecommendations();
}
private loadRecommendations(): void {
const defaultPref: UserPreference = {
favoriteTags: [], allergies: [], maxCalories: 0
};
this.recommendations = recommendEngine.getRecommendations(defaultPref, [], 4);
this.isLoading = false;
}
// ★ 核心:点击卡片 → Want 拉起主应用
private handleRecipeClick(recipe: Recipe): void {
const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext;
const want: Want = {
bundleName: 'com.annan.lingxikitchen',
moduleName: 'entry', // ★ 显式指定主应用模块
abilityName: 'EntryAbility',
parameters: {
recipeId: recipe.id,
recipeName: recipe.name,
recipeIngredients: recipe.ingredients
}
};
ctx.startAbility(want);
}
// ... build() 渲染推荐卡片列表
}
核心点解读:
moduleName: 'entry'--- 必须显式指定,告诉系统要拉起的是主应用模块而非元服务自身- 参数设计 ---
recipeId用于查找完整菜谱数据,recipeName和recipeIngredients用于快速显示(无需二次查询)- HSP 导入 ---
import { recommendEngine } from 'shared',路径简洁且不依赖文件系统层级
Step 6:EntryAbility --- 主应用路由(含 RecipeBridge 参数桥梁)
这是整个跳转链路的核心------EntryAbility 必须在冷启动、热启动、后台唤起三种场景下都正确路由到 RecipeDetailPage。
typescript
// entry/src/main/ets/entryability/EntryAbility.ets
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { RecipeBridge } from 'shared';
export default class EntryAbility extends UIAbility {
private pendingRecipeId: string = '';
private pendingRecipeName: string = '';
private pendingRecipeIngredients: string[] = [];
private windowStage: window.WindowStage | null = null;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.extractRecipeParams(want); // 冷/热启动:提取 Want 参数
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.extractRecipeParams(want); // 后台唤起:提取 Want 参数
this.loadRecipeDetailPage(); // 直接加载详情页
}
onWindowStageCreate(windowStage: window.WindowStage): void {
this.windowStage = windowStage;
// 路由决策:有 recipeId → 直达详情页;否则走登录流程
if (this.pendingRecipeId.length > 0) {
this.loadRecipeDetailPage();
} else {
windowStage.loadContent('pages/LoginPage', ...);
}
}
private extractRecipeParams(want: Want): void {
const recipeId = want?.parameters?.recipeId;
if (recipeId !== undefined && recipeId !== null) {
this.pendingRecipeId = String(recipeId);
this.pendingRecipeName = (want?.parameters?.recipeName as string) ?? '';
this.pendingRecipeIngredients = (want?.parameters?.recipeIngredients as string[]) ?? [];
}
}
// ★ 核心:通过 RecipeBridge 传递参数,再用 loadContent 加载页面
private loadRecipeDetailPage(): void {
RecipeBridge.set(this.pendingRecipeId, this.pendingRecipeName, this.pendingRecipeIngredients);
this.windowStage.loadContent('pages/RecipeDetailPage', callback);
}
}
核心点解读:
- 双入口参数提取 :
onCreate和onNewWant都调用extractRecipeParams,覆盖冷启动和后台唤起windowStage.loadContent而非router.pushUrl:router.pushUrl在 UIAbility 生命周期方法中不可用(无页面上下文),loadContent是唯一可靠方式RecipeBridge参数桥梁 :不依赖LocalStorage(API 23 不可用),不使用router.replaceUrl(会导致二次挂载)。具体原理见第 20.5 篇排错指南- 路由决策 :
pendingRecipeId.length > 0判断是否为元服务跳转,是则跳过登录页直接加载详情
Step 7:RecipeDetailPage --- 双通道参数读取
typescript
// entry/src/main/ets/pages/RecipeDetailPage.ets
import { RecipeBridge } from 'shared';
aboutToAppear(): void {
// ★ 优先级: Router 参数(页面内正常跳转)> RecipeBridge(元服务跳转)
const routerParams = this.getUIContext().getRouter().getParams() as Record<string, Object>;
const params = routerParams ?? this.getParamsFromBridge();
if (params) {
const recipeId = Number(params['recipeId']) || 0;
const recipeName = (params['recipeName'] as string) ?? '';
const recipeIngredients = (params['recipeIngredients'] as string[]) ?? [];
const fullRecipe = recipeManager.getRecipeById(recipeId);
if (fullRecipe) {
this.recipe = fullRecipe;
this.recipe.name = recipeName;
this.recipe.ingredients = recipeIngredients;
}
this.ingredientVM.initFromIngredients(recipeIngredients);
}
}
private getParamsFromBridge(): Record<string, Object> | undefined {
if (RecipeBridge.hasPending()) {
const result: Record<string, Object> = {
'recipeId': RecipeBridge.recipeId,
'recipeName': RecipeBridge.recipeName,
'recipeIngredients': RecipeBridge.recipeIngredients
};
RecipeBridge.clear(); // 读取后清除,避免重复跳转
return result;
}
return undefined;
}
| 通道 | 触发场景 | 参数来源 |
|---|---|---|
Router (getParams()) |
主应用内从首页点击菜谱卡片 | router.pushUrl({ url, params }) |
| RecipeBridge | 元服务卡片点击 → EntryAbility → loadContent | RecipeBridge.set() |
五、代码交付清单
| 文件 | 新增/修改 | 职责 |
|---|---|---|
build-profile.json5(根目录) |
修改 | modules 数组注册 entry、shared、atomicservice |
AppScope/app.json5 |
已有 | bundleType: "app", bundleName: "com.annan.lingxikitchen" |
shared/Index.ets |
修改 | 导出 RecommendEngine、Recipe、UserPreference、MockData、RecipeBridge |
shared/src/main/ets/utils/RecipeBridge.ets |
新增 | 类型安全的静态参数桥梁(EntryAbility ↔ RecipeDetailPage) |
entry/oh-package.json5 |
修改 | 声明 "shared": "file:../shared" 依赖 |
entry/.../module.json5 |
修改 | 新增 ohos.want.action.viewData skill + URI scheme |
entry/.../EntryAbility.ets |
重写 | 双入口参数提取 + RouteBridge + 路由决策(三场景覆盖) |
entry/.../RecipeDetailPage.ets |
修改 | aboutToAppear 双通道回退:Router > RecipeBridge |
entry/.../RecipeManager.ets |
修改 | getRecipeById 参数兼容 `number |
atomicservice/oh-package.json5 |
已有 | 声明 "shared": "file:../shared" 依赖 |
atomicservice/.../module.json5 |
修改 | type: "feature", installationFree: true, skills 添加 uris |
atomicservice/.../Index.ets |
修改 | Want 包含 moduleName: 'entry',from 'shared' 导入 |
六、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 应用类型 | bundleType: "app" |
同时包含主应用和元服务,不适用纯原子化服务类型 |
| 原子化服务模块类型 | type: "feature" |
API 23 中 feature 模块通过 installationFree: true 提供元服务能力 |
| 代码复用方案 | HSP 共享包(shared) | 编译产物收敛为单份,路径稳定,官方推荐 |
| 参数传递方案 | RecipeBridge 静态类 |
不依赖 LocalStorage(API 23 不可用)、不依赖 router.pushUrl(UIAbility 不可用) |
| 冷启动有 recipeId | 直接加载 RecipeDetailPage,跳过登录 | 元服务是"种草"入口,详情页应免登录查看 |
onNewWant 导航 |
windowStage.loadContent |
router.pushUrl 在 UIAbility 生命周期中不可用 |
| 双通道参数读取 | Router 优先,RecipeBridge 兜底 | 兼容主应用内导航和元服务跳转两种场景 |
| 元服务 skills | 添加 uris |
为全局搜索提供索引标识,用户搜索"今天吃什么"可直达服务 |
七、运行与结果验证
7.1 操作步骤
- DevEco Studio 中依次运行三个模块(或直接 Run entry/atomicservice 任一模块,系统会自动部署全部)
- 启动元服务(通过 hdc 命令或全局搜索)→ 加载推荐页,显示 4 道菜谱卡片
- 点击任意卡片 → 主应用被拉起,直接显示 RecipeDetailPage,包含完整食材清单和制作步骤
- 按返回键回到主应用 → 再次点击元服务卡片 → 主应用从后台唤起 → 同样直达详情页
启动元服务的 hdc 命令:
bash
hdc shell aa start -a ohos.want.action.home -b com.annan.lingxikitchen -m atomicservice
7.2 预期日志
收到元服务跳转参数 → recipeId: 7, name: 虾仁蒸蛋
[RecipeBridge] 参数已缓存: id=7, name=虾仁蒸蛋
[RecipeDetail] 路由参数: {"recipeId":"7","recipeName":"虾仁蒸蛋",...}
[RecipeManager] 获取菜谱: id=7, name=虾仁蒸蛋
[IngredientVM] 初始化食材清单(从字符串数组),共 3 项
[RecipeDetail] 菜谱详情加载: 虾仁蒸蛋, 共3步
菜谱详情页加载成功 (元服务跳转)
7.3 路由覆盖矩阵
| 主应用状态 | 触发方法 | 页面 | 参数通道 |
|---|---|---|---|
| 未启动(冷启动) | onCreate → onWindowStageCreate → loadRecipeDetailPage |
RecipeDetailPage | RecipeBridge |
| 已销毁(热启动) | 同上 | 同上 | 同上 |
| 后台运行 | onNewWant → loadRecipeDetailPage |
RecipeDetailPage | RecipeBridge |
| 正常启动 | onWindowStageCreate → loadContent('pages/LoginPage') |
LoginPage | --- |
| 主应用内导航 | 首页 → router.pushUrl |
RecipeDetailPage | Router params |
八、注意事项
- 包名统一 :三个模块共享
bundleName: "com.annan.lingxikitchen",模块级module.json5中无需重复设置,由AppScope/app.json5继承 - 签名一致 :所有模块必须使用相同的调试签名,否则无法互相调用(
startAbility会失败) - 资源文件命名 :entry 和 atomicservice 中的同名资源(如
string.json中的module_desc)需分别定义值,但 string name 可以相同------因为各模块独立读取自己的资源文件 - HSP 不独立运行:shared 模块不能单独部署,只能被 entry/atomicservice 依赖消费
- 勿混用
loadContent和 Router API :loadContent加载的页面不在 Router 栈中,后续调用router.replaceUrl会导致页面二次挂载
九、本阶段总结与下篇预告
今天,我们将《灵犀厨房》的主应用和元服务整合到了一个标准的三模块工程中:
- 三模块一体化架构 :
entry(主应用)+atomicservice(元服务)+shared(HSP 共享包),共享bundleName,统一签名 - 配置层精确定义 :
bundleType: "app"+type: "feature"+installationFree: true,严格遵循 API 23 规范 - 参数传递闭环 :
RecipeBridge静态桥梁 +windowStage.loadContent+ 双通道回退读取,覆盖冷启动/热启动/后台唤起/页面内导航全部场景 - 代码零重复:推荐引擎、数据模型、参数桥梁全部在 shared 中,entry 和 atomicservice 各司其职
现在,用户从桌面搜索"今天吃什么"直达元服务推荐页,点击菜谱卡片,主应用无缝切换到详情页------从"搜索"到"烹饪"的最短路径已经打通。
但这段旅程并非一帆风顺。在实际联调中,我踩了五个陷阱------LoginPage 拦截、router.pushUrl 报错、router.replaceUrl 二次挂载、LocalStorage 编译失败、打补丁循环。这些问题的根源和修复方案,请参考------
下篇预告:第 21 篇《【服务卡片】在桌面查看烹饪进度------主进程强推与跨进程桥接》。
📚 本系列持续更新中:下一篇将详细介绍【服务卡片】在桌面查看烹饪进度------主进程强推与跨进程桥接。
🔗 专栏入口:《HarmonyOS6.1全场景实战》合集
📦 获取基线版本源码包 :包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。
纯血鸿蒙,三模块一体。我们下一篇见!