【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(二十)扩展:【工程集成】主应用 + 元服务 + HSP 共享库——三模块一体化架构
本文介绍了如何将《灵犀厨房》的主应用、元服务和共享库整合到同一工程中,构建三模块一体化架构。关键点包括: 采用bundleType:app而非atomicService,因为应用同时包含主应用和元服务模块 元服务模块通过type:"feature"+installationFree:true实现原子化能力 通过HSP共享包(shared)实现代码复用,避免跨模块直接引用 设计了三模块分层架构:主应
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 三模块架构的编译与运行模型
核心机制:
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
| 维度 | 跨模块直接 import | HSP 共享包 |
|---|---|---|
| 路径稳定性 | 依赖文件系统相对路径 | 通过 oh-package.json5 声明依赖 |
| 编译产物 | 两份副本(各自打包) | 单份共享引用 |
| 官方推荐度 | 非标准用法 | ✅ 官方推荐方案 |
三、架构设计 / 核心逻辑图解
3.1 三模块分层架构
设计原则:
- HSP 集中共享:
RecommendEngine、Recipe模型、MockData、RecipeBridge均位于shared - UI 轻量化:元服务模块只放 UI 代码(1 个 Ability + 1 个页面),不含业务逻辑
- 参数桥梁内置:
RecipeBridge在 shared 模块中,EntryAbility 写入,RecipeDetailPage 读取 - 独立入口:元服务有自己的
AtomicserviceAbility,不依赖EntryAbility
3.2 元服务 → 主应用跳转完整时序
四、实战:三模块工程搭建
前置说明:本章描述的是项目当前的最终状态,配置与代码均来自工程实际文件。如果你在搭建过程中遇到问题,请参考第 20.5 篇《排错指南》中记录的五个陷阱及修复方案。
Step 1:工程级配置 — build-profile.json5 与 AppScope/app.json5
项目根 build-profile.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 — 应用级配置:
{
"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:
{
"name": "shared",
"version": "1.0.0",
"description": "共享库",
"main": "Index.ets",
"dependencies": {}
}
shared/Index.ets — 统一导出入口:
// 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 依赖:
{
"name": "entry",
"version": "1.0.0",
"description": "主应用",
"main": "Index.ets",
"dependencies": {
"shared": "file:../shared"
}
}
entry/src/main/module.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 依赖:
{
"name": "atomicservice",
"version": "1.0.0",
"description": "元服务",
"main": "Index.ets",
"dependencies": {
"shared": "file:../shared"
}
}
atomicservice/src/main/module.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 跳转
// 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。
// 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 — 双通道参数读取
// 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 | string |
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 命令:
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 后端
如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。
纯血鸿蒙,三模块一体。我们下一篇见!
更多推荐



所有评论(0)