当前位置: 首页 > news >正文

【私房菜集 HarmonyOS ArkTS 实战系列 01】从 0 到 1:单机菜谱应用的工程骨架

【私房菜集 HarmonyOS ArkTS 实战系列 01】从 0 到 1:单机菜谱应用的工程骨架

「私房菜集」HarmonyOS ArkTS 实战系列从一个真实可运行的单机菜谱 App 出发,拆解它从工程骨架、内容资产、ArkUI 页面、Preferences 本地状态,到计时器、桌面卡片和发布质量的完整实现链路。本篇聚焦项目的启动层、路由层和目录分层。

一、为什么第一篇先讲工程骨架

很多应用最开始都会从一个首页开始写,但如果一个项目只靠页面一点点堆出来,后面很容易遇到几个问题:

  • 路由字符串散落在各个页面里,页面一多就难维护。
  • 收藏、笔记、最近浏览、计时器等用户状态各写一份,数据很快不一致。
  • 内置内容、用户自建内容和 UI 展示混在一起,后续搜索、分类、详情页都会重复写逻辑。
  • 系统能力入口,比如备份、桌面卡片、启动页和主题模式,没有在工程初期留下位置。

「私房菜集」的定位是一个本地优先的家庭菜谱应用。用户希望它能解决“今天吃什么、怎么找菜、怎么做、做多久、下次如何继续”的完整问题。因此,第一篇先不急着讲某个按钮怎么写,而是先把项目骨架捋清楚:Stage 模型、页面路由、主 Ability、扩展 Ability、分层目录和核心数据流。

二、源码对象总览

源码对象作用
AppScope/app.json5定义应用包名、应用名、版本、图标等 App 级信息。
entry/src/main/module.json5定义 entry 模块、主 Ability、备份扩展、桌面卡片扩展和页面注册入口。
entry/src/main/resources/base/profile/main_pages.json注册应用内 14 个可路由页面。
entry/src/main/ets/common/constants/AppRoutes.ets集中维护路由常量,避免页面里散写字符串。
entry/src/main/ets/pages/Index.ets主 Tab 宿主,承载首页、探索、收藏、我的四个一级入口。
entry/src/main/ets/entryability/EntryAbility.ets应用启动、主题恢复、卡片跳详情等主窗口生命周期逻辑。

这一组文件决定了应用是不是一个“能扩展的项目”,而不只是一个能跑起来的页面。

三、AppScope:先确定应用身份

项目的应用身份定义在AppScope/app.json5中。这里能看到包名、应用名、版本号、图标和 label:

{ "app": { "bundleName": "com.lesson.myapplicationsfcj", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.1", "buildVersion": "1", "icon": "$media:layered_image", "label": "$string:app_name" } }

这一步看起来普通,但它是后续模拟器启动、CSDN 截图、桌面图标、卡片跳转和发布配置的共同基础。本文正文截图通过包名com.lesson.myapplicationsfcj在本地模拟器里直接启动“私房菜集”后截取,而不是用设计稿或静态图片替代。

对应的应用名在entry/src/main/resources/base/element/string.json中维护:

{ "name": "app_name", "value": "私房菜集" }

这样做的好处是,应用名不需要在页面、Ability、桌面卡片里重复写死。资源层统一之后,后续做多语言、上架素材、卡片标题时也更稳。

四、module.json5:Stage 模型应用的能力入口

entry/src/main/module.json5是这一类 HarmonyOS 项目的核心配置文件。它说明当前模块是 entry 类型、运行设备是 phone、主入口是EntryAbility,同时还注册了备份扩展和桌面卡片扩展。

{ "module": { "name": "entry", "type": "entry", "mainElement": "EntryAbility", "deviceTypes": [ "phone" ], "pages": "$profile:main_pages", "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "icon": "$media:app_icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:splash_screen", "startWindowBackground": "$color:start_window_background", "exported": true } ] } }

这段配置里有几个关键点:

  • type: "entry":说明这是应用入口模块。
  • mainElement: "EntryAbility":主窗口从EntryAbility开始。
  • deviceTypes: ["phone"]:当前版本聚焦手机设备。
  • pages: "$profile:main_pages":页面路由不直接写在这里,而是交给 profile 文件维护。
  • startWindowIconstartWindowBackground:启动体验不是默认空白页,而是使用项目资源。

这一层相当于项目的“门厅”:谁来启动、启动时看到什么、能进入哪些页面,都从这里开始。

五、14 个页面先注册,再谈功能拆解

「私房菜集」当前注册了 14 个页面:

{ "src": [ "pages/Index", "pages/RecipeDetailPage", "pages/SearchResultPage", "pages/CategoryRecipeListPage", "pages/AddRecipePage", "pages/TimerPage", "pages/NotePage", "pages/DietPreferencePage", "pages/SettingsPage", "pages/ShareRecipePage", "pages/UnitConverterPage", "pages/BackupRestorePage", "pages/PrivacyDataPage", "pages/AboutPage" ] }

这 14 个页面不是随意堆出来的,它们对应的是一条完整用户路径:

首页推荐 → 探索/搜索/分类 → 菜谱详情 → 收藏/想做/计时/笔记/分享 → 我的菜谱/饮食偏好/设置/备份/隐私/关于

第一版就把页面注册完整,有两个好处。

第一,路由边界清晰。首页、探索、收藏、我的属于一级 Tab,不需要每次切换都入栈;详情、搜索、添加、设置等属于二级页面,适合通过router.pushUrl打开。

第二,后续文章可以按真实工程链路拆分,而不是每篇都临时补路由。比如第二篇讲主 Tab,第三篇讲内容数据源,第六篇讲详情页,第十篇讲计时器,都是建立在这份页面注册表之上的。

六、路由常量:不要让字符串到处飞

页面注册之后,项目没有在每个页面里反复写pages/RecipeDetailPage这样的字符串,而是用AppRoutes.ets集中管理:

export class AppRoutes { static readonly INDEX: string = 'pages/Index'; static readonly DETAIL: string = 'pages/RecipeDetailPage'; static readonly SEARCH: string = 'pages/SearchResultPage'; static readonly CATEGORY: string = 'pages/CategoryRecipeListPage'; static readonly ADD_RECIPE: string = 'pages/AddRecipePage'; static readonly TIMER: string = 'pages/TimerPage'; static readonly NOTE: string = 'pages/NotePage'; static readonly DIET_PREFERENCE: string = 'pages/DietPreferencePage'; static readonly SETTINGS: string = 'pages/SettingsPage'; static readonly SHARE: string = 'pages/ShareRecipePage'; static readonly UNIT_CONVERTER: string = 'pages/UnitConverterPage'; static readonly BACKUP_RESTORE: string = 'pages/BackupRestorePage'; static readonly PRIVACY_DATA: string = 'pages/PrivacyDataPage'; static readonly ABOUT: string = 'pages/AboutPage'; }

这个文件很小,但工程价值很高。后续如果页面路径调整,只需要改常量,不需要在十几个页面里逐个搜索字符串。

例如首页点击菜谱进入详情时,调用的是集中路由:

private openDetail(recipeId: string): void { router.pushUrl({ url: AppRoutes.DETAIL, params: { recipeId } }); }

对这种多页面应用来说,路由集中管理是非常值得早做的基础设施。

七、主 Tab:四个一级入口的产品骨架

Index.ets是整个应用的主 Tab 宿主。它维护四个一级入口:

export type AppTabKey = 'home' | 'explore' | 'favorite' | 'mine'; export type FavoriteTabKey = 'favorite' | 'todo'; export const APP_TABS: BottomTabItem[] = [ { key: 'home', title: '首页', icon: '⌂' }, { key: 'explore', title: '探索', icon: '⌕' }, { key: 'favorite', title: '收藏', icon: '♡' }, { key: 'mine', title: '我的', icon: '○' } ];

这里用的是窄类型,而不是普通字符串。这样selectedTab只能是home / explore / favorite / mine四种之一,后续切换 Tab 时不容易传错。

Index.ets中,页面会根据当前 Tab 渲染不同 Builder:

build() { Column() { Stack() { if (this.selectedTab === 'home') { this.HomeTab() } else if (this.selectedTab === 'explore') { this.ExploreTab() } else if (this.selectedTab === 'favorite') { this.FavoriteTab() } else { this.MineTab() } } .layoutWeight(1) BottomNavBar({ selectedKey: this.selectedTab, onChange: (key: AppTabKey) => { this.switchTab(key); } }) } }

这种结构的重点是:一级 Tab 切换不进入新页面栈,而是在同一个 Index 宿主内切换内容;只有详情、添加菜谱、设置这类二级页面才走路由。

这也是菜谱类应用比较自然的信息架构:

  • 首页解决“今天看什么”。
  • 探索解决“需要找什么”。
  • 收藏解决“需要保留什么”。
  • 我的解决“自建内容、个人偏好、应用设置如何管理”。

八、EntryAbility:启动、主题和卡片跳转的总入口

EntryAbility.ets不只是负责加载首页。它还做了两个值得关注的工作:启动时恢复主题,以及处理桌面卡片跳转详情页。

启动阶段会初始化本地存储,并应用保存过的主题模式:

onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { try { localStoreAdapter.init(this.context); settingsService.applySavedTheme(this.context); this.handleCardRouterWant(want); } catch (err) { hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err)); } }

窗口创建后加载主页面:

onWindowStageCreate(windowStage: window.WindowStage): void { try { const themeMode = settingsService.getSettings().themeMode; const backgroundColor = themeMode === 'dark' ? DARK_START_WINDOW_BG : LIGHT_START_WINDOW_BG; windowStage.getMainWindowSync().setWindowBackgroundColor(backgroundColor); } catch (err) { hilog.error(DOMAIN, 'testTag', 'Failed to set start window background. Cause: %{public}s', JSON.stringify(err)); } windowStage.loadContent('pages/Index', (err) => { if (err.code) { hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); return; } this.isContentLoaded = true; this.openPendingRecipeDetail(); }); }

这里有一个细节:桌面卡片可能在应用内容加载完成之前就传入了目标菜谱 ID,所以EntryAbility使用pendingRecipeId临时保存,等pages/Index加载完成后再跳详情页。

private openPendingRecipeDetail(): void { if (!this.isContentLoaded || this.pendingRecipeId.length === 0) { return; } const recipeId = this.pendingRecipeId; this.pendingRecipeId = ''; router.pushUrl({ url: 'pages/RecipeDetailPage', params: { recipeId, source: 'widget' } }); }

这说明项目骨架已经为后续系统能力预留了入口。第十三篇的桌面卡片主题会继续拆解这里的跳转链路。

九、扩展 Ability:备份和桌面卡片已经预埋

module.json5中还注册了两个扩展:

{ "name": "EntryBackupAbility", "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", "type": "backup", "exported": false, "metadata": [ { "name": "ohos.extension.backup", "resource": "$profile:backup_config" } ] }
{ "name": "EntryFormAbility", "srcEntry": "./ets/entryformability/EntryFormAbility.ets", "label": "$string:EntryFormAbility_label", "description": "$string:EntryFormAbility_desc", "type": "form", "metadata": [ { "name": "ohos.extension.form", "resource": "$profile:form_config" } ] }

对于第一篇来说,不需要展开备份和桌面卡片的实现细节,但要知道它们已经进入工程骨架:

  • EntryBackupAbility:后续承接本地数据备份与恢复。
  • EntryFormAbility:后续承接“今日菜谱”桌面卡片。
  • form_config.json:定义卡片尺寸、更新时间、默认维度和 ArkTS UI 入口。

这也是项目一开始就按“完整应用”来组织的证据。

十、工程分层:页面只组合,服务管业务,仓储管持久化

entry/src/main/ets目录看,项目已经形成了比较清晰的分层:

entry/src/main/ets/ common/ constants/ components/ common/ recipe/ timer/ entryability/ entrybackupability/ entryformability/ models/ pages/ repositories/ services/ widget/

这个分层和 HarmonyOS 单机应用的开发节奏很匹配:

  • pages/:路由级页面,负责生命周期、页面状态和页面组合。
  • components/:只做可复用 UI,比如菜谱卡片、底部导航、计时圆环。
  • models/:定义领域类型,比如RecipeRecipeSummaryTimerSession
  • services/:组织业务读写,比如菜谱聚合、搜索、收藏、计时、偏好。
  • repositories/:封装本地 Preferences 读写,页面不直接碰持久化 API。
  • common/:放路由、Tab、主题 token 等共享常量。

这套结构让项目可以继续扩展。比如第三篇会讲RecipeDataSource如何从 rawfile 读取 514 道菜;第七篇会讲LocalStoreAdapterUserStateRepository如何把收藏、想做、最近浏览、笔记统一成一个本地状态仓。

十一、从运行截图看骨架是否真的成立

正文使用的是本机模拟器真实运行截图。启动命令如下:

hdc shell aa start -a EntryAbility -b com.lesson.myapplicationsfcj

截图命令如下:

hdc shell snapshot_display -f /data/local/tmp/sfcj_01_home.jpeg hdc file recv /data/local/tmp/sfcj_01_home.jpeg .\SFCJ\screenshots\01_home_raw.jpeg

从截图可以验证几个点:

  • 顶部应用名是“私房菜集”,不是默认模板。
  • 首页已加载真实菜谱图片和菜谱标题。
  • 页面包含最近浏览、热门精选、随机厨房等模块。
  • 底部四 Tab 是首页、探索、收藏、我的。
  • 主色和应用气质已经从默认工程转成了菜谱应用的暖色风格。

这说明工程骨架已经不是“配置文件写好了但页面没承接”,而是配置、路由、页面、数据和资源都串起来了。

十二、第一篇验收清单

这一篇对应的是项目启动层和工程骨架层,验收重点不是某个业务按钮,而是整体结构是否成立:

  • AppScope/app.json5中包名、应用名、图标、版本号明确。
  • entry/src/main/module.json5使用 Stage 模型 entry 模块。
  • EntryAbility能正常加载pages/Index
  • main_pages.json注册了当前应用需要的 14 个页面。
  • AppRoutes.ets集中维护页面路径。
  • Index.ets作为主 Tab 宿主,承载首页、探索、收藏、我的。
  • 应用能在本机模拟器启动,并截取真实首页截图。
  • 首页截图中能看到真实菜谱数据和底部四 Tab。

如果这些检查都通过,后续再拆首页推荐流、搜索、详情、收藏和计时器时,就不是在空地上补功能,而是在一个清晰工程骨架里继续迭代。

十三、问题复盘:为什么不把所有逻辑都写在首页

这个项目有一个很容易踩的坑:菜谱应用看起来可以只写一个大首页,里面放搜索、分类、收藏、详情弹层和设置入口。但这种写法到后期会非常痛苦。

原因有三点。

第一,菜谱数据天然会被多个页面复用。首页需要推荐数据,探索页需要分类和列表,详情页需要完整菜谱,搜索页需要本地匹配。如果数据查询散在页面里,后续很难保证一致。

第二,用户状态不是某个页面的私有状态。收藏、想做清单、最近浏览、笔记、忌口偏好都要跨页面读取和更新,必须尽早有 repository/service 分层。

第三,系统能力不是 UI 装饰。桌面卡片、备份、启动页、深色模式、分享页,都需要从模块配置和 Ability 层进入项目,而不是等页面写完再临时补。

所以第一篇先讲骨架,是为了后面的每个模块都有位置可放、有边界可守。

十四、下一篇预告

下一篇进入Index.etsBottomNavBar.ets,专门拆“主 Tab 宿主”这一层:

  • 首页、探索、收藏、我的为什么放在同一个宿主页面。
  • selectedTab如何控制四个一级入口。
  • aboutToAppearonPageShow如何让返回主页面后数据刷新。
  • 底部导航如何组件化,避免每个页面重复写选中态。

也就是说,第一篇先把门厅、路由和房间编号搭好;第二篇开始看用户每天真正进入的主界面。

http://www.jsqmd.com/news/1123798/

相关文章:

  • ORIN NX 16G + ubuntu22.04 环境安装及模型部署
  • 终极指南:40+经典DSGE模型库如何加速你的宏观经济研究
  • FigmaCN:5分钟快速汉化Figma界面,中文设计师的完整解决方案
  • Nutstore Sync 和 WebDAV 有什么区别?Obsidian 坚果云同步新旧方案完整对比
  • 角谷猜想的弗洛伊德算法的同构映射:数论映射图论 Version6.6
  • HoRain云--Java Applet
  • 独立开发实战:学生管理+考试防作弊机制设计
  • laserMapping.cpp 中的 sync_packages() 详细讲解
  • 如何永久保存微信聊天记录:简单三步实现数据自主管理终极指南
  • 掌握专业级Windows Defender控制:高效系统安全防护管理实战指南
  • 彻底掌控你的Windows“此电脑“:MyComputerManager让顽固图标消失无踪
  • 深耕低代码5年,终于遇见打破行业桎梏的AI原生平台
  • 不受待见的钻石又火了?新娘不要英伟达为啥抢着要?
  • Obsidian插件汉化终极指南:3种模式快速实现英文插件中文化
  • GTA5终极修改器YimMenu:10分钟快速上手指南
  • 50. 怎么给OrCAD封装库添加新的属性?I Cadence Allegro 电子设计 快问快答
  • Shell的基础知识和常用命令
  • OpenClaw:AI智能体开发的高效跨平台解决方案
  • CUE: Concept-Aware Multi-Label Expansion to Mitigate Concept Confusion in Long-Tailed Learning
  • PIC32与25CSM04 SPI EEPROM高速数据检索实现
  • 5分钟解锁你的音乐宝库:qmcdump音频格式转换工具完全指南
  • 强力解锁喜马拉雅音频自由:跨平台下载神器XMly-Downloader-Qt5深度解析
  • 终极智能控制:用Turbo Boost Switcher重新掌控你的Mac性能体验
  • 蛋糕烘焙小程序|实用线上展示页面设计分享
  • Office批量打印软件推荐,告别低效操作
  • Python 语法基础 IO
  • Java非对称加密实战:RSA、DSA、ECC算法对比与选型指南
  • C++中的STL与标准库算法
  • 杭州创始人IP打造运营如何进行?
  • 通过kickstart 执行mysql、clickhouse数据导入