【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十)扩展:【工程集成】主应用 + 元服务 + HSP 共享库——三模块一体化架构

HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十)扩展:【工程集成】主应用 + 元服务 + HSP 共享库------三模块一体化架构

摘要 :第 19 篇我们为《灵犀厨房》装上了通知系统------延时提醒 + WantAgent 回到应用。第 20 篇我们构建了一键推荐原子化服务。但这两个模块目前还各居一隅------主应用的登录页、首页、详情页在一个工程,元服务的推荐页在另一个工程。用户从元服务点击菜谱卡片,主应用需要正确接收 Want 参数并直接打开 RecipeDetailPage------这要求两个模块必须在同一工程、同一 bundleName 下运行。本篇,我们将把 entry(主应用)、atomicservice(元服务)、shared(HSP 共享库)三个模块整合到一个工程中,构建标准的三模块一体化架构。严格遵循 HarmonyOS 6.1.0(API 23)规范,代码基于工程实际文件。


一、引言与系列定位

经过前 19 篇的积累,《灵犀厨房》的主应用已经非常完备。第 20 篇又新增了原子化服务模块。但要实现"元服务点击 → 主应用详情页"的闭环,两个模块必须合并到同一个工程------共享 bundleName、共享 HSP 库、共享签名。

这不仅仅是"把两个文件夹放在一起"。它涉及三个关键决策:

决策 问题 本篇答案
应用类型 bundleTypeapp 还是 atomicService app------因为同时包含主应用和元服务
模块类型 atomicservice 的 module.json5type 用什么? 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),不独立运行,只被消费
  • entryatomicservice 编译为 .hap(Harmony Ability Package),各自独立
  • 三个模块共享同一个 bundleName------系统将它们视作同一个应用的不同模块
  • HSP 在最终包中只存在一份副本,entry 和 atomicservice 共享引用

2.2 原子化服务的两种模块声明方式

在 HarmonyOS 中,原子化服务有两种声明方式,取决于应用的整体形态:

场景 AppScope/app.json5bundleType 模块 module.json5type 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

★ 跨模块参数桥梁

设计原则

  1. HSP 集中共享RecommendEngineRecipe 模型、MockDataRecipeBridge 均位于 shared
  2. UI 轻量化:元服务模块只放 UI 代码(1 个 Ability + 1 个页面),不含业务逻辑
  3. 参数桥梁内置RecipeBridge 在 shared 模块中,EntryAbility 写入,RecipeDetailPage 读取
  4. 独立入口 :元服务有自己的 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.json5AppScope/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://recipe URI 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 类型,而非 atomicService
  • installationFree: 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 用于查找完整菜谱数据,recipeNamerecipeIngredients 用于快速显示(无需二次查询)
  • 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);
  }
}

核心点解读

  • 双入口参数提取onCreateonNewWant 都调用 extractRecipeParams,覆盖冷启动和后台唤起
  • windowStage.loadContent 而非 router.pushUrlrouter.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;
}
通道 触发场景 参数来源
RoutergetParams() 主应用内从首页点击菜谱卡片 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 操作步骤

  1. DevEco Studio 中依次运行三个模块(或直接 Run entry/atomicservice 任一模块,系统会自动部署全部)
  2. 启动元服务(通过 hdc 命令或全局搜索)→ 加载推荐页,显示 4 道菜谱卡片
  3. 点击任意卡片 → 主应用被拉起,直接显示 RecipeDetailPage,包含完整食材清单和制作步骤
  4. 按返回键回到主应用 → 再次点击元服务卡片 → 主应用从后台唤起 → 同样直达详情页

启动元服务的 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 路由覆盖矩阵

主应用状态 触发方法 页面 参数通道
未启动(冷启动) onCreateonWindowStageCreateloadRecipeDetailPage RecipeDetailPage RecipeBridge
已销毁(热启动) 同上 同上 同上
后台运行 onNewWantloadRecipeDetailPage RecipeDetailPage RecipeBridge
正常启动 onWindowStageCreateloadContent('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 APIloadContent 加载的页面不在 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 编译失败、打补丁循环。这些问题的根源和修复方案,请参考------

📚 配套排错指南第 20.5 篇《元服务跳转主应用------Want 参数传递的五个陷阱与架构修复》

下篇预告:第 21 篇《【服务卡片】在桌面查看烹饪进度------主进程强推与跨进程桥接》。


📚 本系列持续更新中:下一篇将详细介绍【服务卡片】在桌面查看烹饪进度------主进程强推与跨进程桥接。

🔗 专栏入口《HarmonyOS6.1全场景实战》合集

📦 获取基线版本源码包包括第1-15篇所有代码 + 架构文档 + Flask 后端

如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。

纯血鸿蒙,三模块一体。我们下一篇见!

相关推荐
若兰幽竹15 小时前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十点五):【排错指南】元服务跳转主应用——Want 参数传递的五个陷阱与架构修复
元服务·华为鸿蒙系统·harmonyos6.1·排除指南
若兰幽竹2 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(二十):【元服务】一键烹饪推荐原子化服务——免安装直达美味
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹2 天前
HarmonyOS 6.1 开发者盛宴|《灵犀厨房》实战(十九):【通知系统】延时烹饪提醒——让通知不再错过关键步骤
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹7 天前
HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(十八):【手表协同】烹饪计时器流转至智能手表——手腕掌控烹饪节奏
智能手表·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹10 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十七):【语音识别】免提声控启动播报——动口不动手
语音识别·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹11 天前
【HarmonyOS6.1全场景实战】基线版本:我用了15篇文章,造出了一个能登录、能推荐、带后台的鸿蒙全栈App
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹12 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十五)之【超级设备模拟器实战】多设备交互调试:像上帝一样俯瞰整个智能厨房
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹12 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十四)之【分布式流转】让菜谱“飞”:手机选、平板看、智慧屏播的全场景秘诀
分布式·华为鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹13 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十三)之【智能厨电模拟】用代码“凭空”创造智能厨房:《灵犀厨房》的全场景前奏
华为鸿蒙系统·harmonyos6.1.0·灵犀厨房