
做一个菜谱类应用表面看是页面和列表往深一点看其实是一个很典型的端侧软件工程问题。中式美食这个项目里有首页推荐、菜品列表、菜品详情、收藏笔记、最近浏览、视频拆菜、个人厨房这些页面。每个页面单独写出来并不难难的是页面之间要能稳定流转数据要能留下来图片资源不能乱用户编辑过的内容不能因为切换页面或重启应用就丢掉。这也是我把这篇投到嵌入式全栈活动里的原因。它不是硬件驱动文章但它解决的是端侧应用里很真实的一组问题资源怎么被系统识别状态怎么在页面之间传递本地数据怎么落盘复杂编辑流程怎么拆成可验证的小状态。一、先说项目环境项目说明应用中式美食平台HarmonyOS NEXT / HarmonyOS 6工具DevEco Studio 6.0.0 ReleaseSDKHarmonyOS SDK API 20语言与框架ArkTS / ArkUI V2 / Stage 模型核心能力资源注册、路由状态、本地持久化、视频拆菜编辑这篇不是把某个 demo 跑通而是从已上架应用里抽出几条稳定链路来看。我的判断标准也很简单用户点了什么、页面怎么变、数据怎么存、再次打开还能不能恢复。二、图片资源不能靠字符串临时拼菜谱应用最容易踩的坑是图片。很多人一开始会想数据里存一个图片名然后运行时拼成资源路径。constimageName:stringdish_d001;Image($r(app.media.imageName));这个思路在普通 Web 图片路径里很常见但在 HarmonyOS 资源体系里不合适。$r(app.media.xxx)更像编译期资源引用不应该依赖运行时字符串乱拼。中式美食的做法是先做一个图片资源注册表constREG:Recordstring,Resource{dish_d001:$r(app.media.dish_d001),dish_d002:$r(app.media.dish_d002),home_card_casserole:$r(app.media.home_card_casserole)};exportfunctiongetImage(key:string):ResourceStr|undefined{if(!key)returnundefined;if(key.startsWith(file://)||key.startsWith(/data/)||key.startsWith(datashare://)){returnkey;}returnREG[key];}这样数据层只保存稳定的图片 key组件层只管调用getImage()。至于图片来自内置资源还是后续用户上传的文件路径统一在这个入口里处理。这个改动看起来不大但收益很明显DishCard、DishTile、CuisineDishTile都可以复用同一套图片逻辑。某条数据缺图时页面走兜底不会出现有的页面正常、有的页面空白的情况。三、路由状态要有一条清楚的主线中式美食有五个一级入口首页、分类、收藏、课堂、我的。每个入口下面又会进详情页、筛选页、笔记页、视频拆菜页。如果所有页面都靠临时字符串跳转越写越容易乱。尤其是底部 Tab 和二级页面共存时最容易出现两个问题问题结果Tab 状态和二级页状态混在一起返回时不知道该回首页还是回上一级路由参数散落在页面里同一个菜品详情页从不同入口进来表现不一致所以项目里把路由拆成三层exportenumMainTab{Homehome,Categorycategory,Favoritefavorite,Coursecourse,Profileprofile}exportinterfaceNavState{currentTab:MainTab;selectedDishId?:string;selectedCategoryId?:string;}页面不直接猜当前状态而是通过统一状态去判断现在在哪个 Tab下一级页面带了什么参数返回时应该退到哪里。这个设计的好处不是“代码更优雅”这么空而是用户体验更稳定。比如用户从搜索结果进入详情再收藏一道菜返回时应该回到搜索结果而不是突然跳回首页。路由状态清楚以后这类问题更容易验收。四、本地持久化要按业务仓储拆开收藏、笔记、最近浏览都要落在本地。最简单的写法是页面直接读写Preferences但这样很快会把页面写脏。中式美食把本地存储拆成三层层级职责PrefsStore只负责 Preferences 的读写AppStorageDao负责 JSON 序列化、反序列化和 key 管理Repository负责收藏、笔记、最近浏览的业务规则例如最近浏览不是简单追加它要去重、置顶、限制最大数量exportclassViewedRepository{addViewed(dishId:string):void{constoldList:string[]this.list();constnextList:string[][dishId,...oldList.filter((id)id!dishId)].slice(0,100);this.dao.setStringArray(viewed_dishes,nextList);}}这个规则如果写在页面里详情页、首页、个人页都会复制一份。写进 Repository 后页面只表达“用户看过这道菜”仓储自己处理去重和数量上限。我验收这块时主要看六件事步骤操作预期1收藏一道菜收藏仓储写入 id2重启应用收藏状态可以恢复3重复浏览同一道菜最近浏览只保留一条并移动到顶部4连续写入 105 条浏览记录只保留最新 100 条5新增一条笔记笔记列表和我的页面统计同步6删除本地资产页面统计回到 0不残留旧状态五、视频拆菜不是结果页而是编辑流程视频拆菜页最容易做成一个“识别结果展示页”。但真正给用户用时识别结果一定会有不确定、不完整、不符合个人习惯的地方。所以中式美食把视频拆菜做成草稿编辑流程。核心模型不是最终菜谱而是一份可以被人工校对的VideoRecipeInsightexporttypeVideoInsightStatusdraft|confirmed;exporttypeVideoFactorStatusdetected|uncertain|edited;exporttypeVideoFactorKindingredient|step|heat|cue|risk;exportinterfaceVideoFactor{id:string;kind:VideoFactorKind;text:string;timeLabel:string;status:VideoFactorStatus;}这里的factor可以是食材、步骤、火候、提示也可以是风险提醒。每一条都有自己的状态。这样做有一个很实际的好处页面不需要把一大段识别文字重新解析来解析去。用户改了某一条就把这一条标成edited用户确认整份草稿就把草稿状态改成confirmed。六、这条链路怎么复现如果要复现这套端侧链路我建议按下面的顺序检查不要一上来就看代码打开首页确认推荐卡片图片正常显示。进入列表页确认不同卡片组件图片高度稳定。进入详情页收藏一道菜再返回收藏页检查状态。重启应用确认收藏、最近浏览和笔记仍然存在。打开视频拆菜页输入一段做菜文字生成草稿。修改某条食材或步骤确认状态从detected变成edited。保存草稿再从我的厨房入口重新打开。这套验收比“页面能打开”更有意义。因为端侧应用的问题通常不是第一眼看不到而是切页面、重启、重复操作以后才暴露。七、踩坑和取舍第一资源要早一点收口。图片数量少的时候手写判断看不出问题图片一多多个组件就会开始分叉。第二路由不要只看跳转成功。真正要看的是返回、Tab 切换、参数恢复这些才是用户每天会碰到的场景。第三持久化不要直接塞进页面。页面越干净后面做测试、做状态恢复越容易。第四视频拆菜这种功能不要假设识别结果天然正确。只要给用户编辑空间就必须给每条内容设计状态。小结中式美食这个项目给我的一个感受是端侧应用的复杂度不是来自某一个大功能而是来自很多小状态要稳定地连在一起。图片资源、路由、本地存储、视频拆菜看起来是四件事但放到真实应用里它们最后都会落到同一个问题上用户做过的操作应用能不能记住用户下一次回来页面能不能给出一致的结果。把这些链路拆清楚才算真正把一个 HarmonyOS 应用从“能跑”推进到“能长期用”。