HarmonyOS旅行探索——架构总览与通用模式提炼 📅 2026/7/12 22:13:29 旅行探索 App 做到现在已经覆盖了 10 个页面和 2 个服务层文件。回过头看整个项目会发现很多在单篇文章里讲不清楚的规律——哪些 Builder 被复用了多少次哪些布局模式反复出现数据流在页面之间是怎么流转的。这篇把所有页面摊开做一次全局性的梳理。完整效果项目全貌pages/ Index.ets 探索首页 CityDetail.ets 城市详情景点美食 TripPlanner.ets 行程规划 CuisinePage.ets 美食指南 FoodDetail.ets 美食详情 CurrencyPage.ets 汇率换算 PhraseBook.ets 旅行短语 ToolsPage.ets 旅行工具箱 WeatherPage.ets 旅行天气 ProfilePage.ets 个人中心 services/ TravelData.ets 城市/景点/美食/短语数据 UserDataService.ets 用户状态收藏/打卡10 个页面2 个服务层。页面之间通过router.pushUrl跳转数据在服务层集中管理。页面间的数据流整个 App 的数据流向可以画成一张图TravelData (城市/景点/美食/短语 mock 数据) │ ├→ Index (读取 CITIES展示城市网格) │ ├→ CityDetail (传 cityId查找城市详情) │ │ └→ FoodDetail (传全字段美食详情) │ ├→ TripPlanner (本地状态) │ ├→ CuisinePage (聚合所有城市的美食) │ │ └→ FoodDetail (传全字段) │ ├→ CurrencyPage (本地汇率数据) │ ├→ PhraseBook (读取 PHRASES 字典) │ ├→ WeatherPage (读取 CITIES 本地天气 mock) │ └→ ToolsPage (聚合入口 本地状态) │ └→ ProfilePage ├→ UserDataService (读取用户状态) └→ 各功能页面 (跳转)三个关键节点Index是分发中心——从 TravelData 读取城市列表根据用户操作分发到各个子页面CityDetail是数据中转站——接收 ID查找完整数据展示嵌套内容再把部分字段传给 FoodDetailUserDataService是状态仓库——各页面的用户操作收藏、打卡汇总到这里ProfilePage 从这里读取聚合数据路由跳转的两种模式整个 App 的路由跳转只有两种模式模式一ID 查询// 跳转方router.pushUrl({url:pages/CityDetail,params:{cityId:city.id}asRecordstring,Object})// 接收方constid:numberp[cityId]asnumberfor(letinumber0;iCITIES.length;i){if(CITIES[i].idid){...}}使用场景CityDetail、ExerciseDetail运动健康 App。适用于数据量大、需要从统一数据源查找的场景。模式二全字段传递// 跳转方router.pushUrl({url:pages/FoodDetail,params:{name:f.name,emoji:f.emoji,...}asRecordstring,Object})// 接收方if(p[name])this.namep[name]asstring使用场景FoodDetail。适用于数据量小、只在当前场景使用的场景。两种模式的选择标准数据量 5 个字段用 ID 查询≤ 5 个字段用全字段传递。Builder 复用统计统计整个项目里所有 Builder 的使用情况Builder所在页面被调用次数是否跨页面复用NavBtnIndex1否ToolBtnIndex1否ToolCardToolsPage1否TimeCardToolsPage1否WDetailWeatherPage1否ForecastRowWeatherPage1否ChipCityDetail1否StatProfilePage1否MenuProfilePage1否但结构和运动健康 App 一致DivProfilePage1否但结构和运动健康 App 一致DataCardBodyDataPage1否GoalRowGoalPage1否CurrencyPickerCurrencyPage2页面内复用ForecastRowWeatherPage1否整个项目没有跨页面复用的 Builder——每个 Builder 都只在自己的页面里使用。唯一的页面内复用是 CurrencyPage 的 CurrencyPicker被源货币和目标货币两个地方调用。这和运动健康 App 的情况一样。Builder 在 ArkTS 里的定位是页面内的轻量复用不是跨页面的通用组件。跨页面的通用 UI 需要用Component实现Builder 适合处理同一页面里出现两次以上的相似结构。颜色常量的跨项目一致性常量值用途运动健康 App旅行探索 AppA主色强调、按钮、标签#FF4757红#FF6B35橙T1#1E1B2E标题文字相同相同T2#888888副文字相同相同T3#BBBBBB辅助灰相同相同BG#F8F7FC页面背景相同相同部分页面用 #FFF8F5CW#FFFFFF卡片白相同相同T1/T2/T3 和 BG/CW 在两个 App 里完全一致——它们是中性色不承载品牌调性只负责文字层次和背景。A主色根据 App 主题变化运动用红色旅行用橙色。这种中性色固定、主题色可变的模式是一个可靠的设计系统基础。新增 App 时只需要定义自己的主色 A其他颜色直接复用。四个 Builder 的三种用途整个项目含运动健康 App里的 Builder 可以归纳为三种用途用途一条件分支// 用于 ForEach 内部的条件渲染if(item.done){Text(✓)...}Builder 不直接参与条件分支但if/else经常出现在 Builder 内部如 AchievementPage 的 earned 检查。用途二列表模板// 用于 ForEach 的每个元素BuilderForecastRow(day:string,icon:string,high:string,low:string){...}ForEach(this.forecast,(f){this.ForecastRow(...)})这是 Builder 最常见的用途。当 ForEach 的每个元素结构复杂多层嵌套、多个属性时抽成 Builder 让 build() 方法保持整洁。用途三可交互的回调容器// 接收 () void 回调绑定到 onClickBuilderMenu(label:string,icon:string,action:()void){Row(){...}.onClick(action)}当同一个 UI 结构需要在不同地方执行不同操作时用回调参数让 Builder 保持通用性。数据层的设计取舍旅行 App 的数据层有一个明显的特征——大部分数据是只读的 mock。TravelData.ets城市、景点、美食、短语全部写死UserDataService.ets用户状态收藏/打卡运行时维护各页面的本地 State行程规划的 days、小费计算器的 amount、天气页面的 selectedCity只有 TripPlanner、CurrencyPage、ToolsPage 的出行清单有用户可修改的数据而且这些数据目前都在State里没有持久化存储。刷新页面或退出 App 后数据会重置。这是原型阶段的合理取舍。真正的 App 需要接入ohos.data.preferences本地持久化或服务器 API云端同步但目前的重点是验证 UI 和交互逻辑数据持久化是后续的工作。通用模式提炼回顾整个项目可以提炼出六个在所有页面里反复出现的模式模式一Stack Scroll Column 骨架几乎所有页面都用这个结构——Stack 叠加固定元素导航栏/底部导航和可滚动内容区。模式二导航栏的固定结构34×34 圆形返回按钮 标题 右侧操作区。这个结构在 10 个页面里出现了 9 次Index 用底部导航栏代替。模式三白色卡片 浅灰背景backgroundColor(#FFFFFF).borderRadius(14-18)的白色卡片放在#F8F7FC的浅灰背景上。这是整个 App 的视觉基础。模式四State 数组的引用变更刷新修改数组元素后必须复制新数组并替换引用否则 UI 不刷新。这个模式在 TripPlanner、ToolsPage 出行清单里反复出现。模式五条件渲染控制区块显隐if (this.saved.length 0)控制已保存的城市区域if (this.city)控制详情页内容区。空数据时不显示对应 UI而不是显示空白占位。模式六回调参数实现 Builder 的通用化Menu、ToolBtn、ToolCard 等 Builder 都接收() void回调让同一个 Builder 能在不同场景执行不同操作。这六个模式不是最佳实践而是在当前项目规模下自然演化出来的约定。它们够用、好理解、容易复现。当项目规模增长到需要更好的可维护性时再考虑引入更系统的架构方案也不迟。