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

【私房菜集 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.json5Stage 模型应用的能力入口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 文件维护。startWindowIcon和startWindowBackground启动体验不是默认空白页而是使用项目资源。这一层相当于项目的“门厅”谁来启动、启动时看到什么、能进入哪些页面都从这里开始。五、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 渲染不同 Builderbuild() { 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/定义领域类型比如Recipe、RecipeSummary、TimerSession。services/组织业务读写比如菜谱聚合、搜索、收藏、计时、偏好。repositories/封装本地 Preferences 读写页面不直接碰持久化 API。common/放路由、Tab、主题 token 等共享常量。这套结构让项目可以继续扩展。比如第三篇会讲RecipeDataSource如何从 rawfile 读取 514 道菜第七篇会讲LocalStoreAdapter和UserStateRepository如何把收藏、想做、最近浏览、笔记统一成一个本地状态仓。十一、从运行截图看骨架是否真的成立正文使用的是本机模拟器真实运行截图。启动命令如下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.ets和BottomNavBar.ets专门拆“主 Tab 宿主”这一层首页、探索、收藏、我的为什么放在同一个宿主页面。selectedTab如何控制四个一级入口。aboutToAppear和onPageShow如何让返回主页面后数据刷新。底部导航如何组件化避免每个页面重复写选中态。也就是说第一篇先把门厅、路由和房间编号搭好第二篇开始看用户每天真正进入的主界面。