尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【时光清单|14】HarmonyOS ArkTS 主导航实战:统一页面入口、返回路径和参数校验

【时光清单|14】HarmonyOS ArkTS 主导航实战:统一页面入口、返回路径和参数校验 【时光清单14】HarmonyOS ArkTS 主导航实战统一页面入口、返回路径和参数校验主导航最怕“能跳过去但无法证明跳得对”。首页、全部列表和设置页各自拼路由字符串详情页接收一段 JSON解析失败后仍显示默认对象筛选页把任意字符串当作类型某些页面用共享NavPathStack另一些又创建新栈。单次点击可能看不出问题等到系统返回、连续进入详情、页面恢复或参数结构升级时导航状态就会分叉。时光清单的真实源码建立了一条统一主链AppStore.bootstrap()在AppStorage中创建唯一NavPathStackIndex.ets用它构建根NavigationRouteNames.ets集中声明二级页面名称RouteMap.ets把名称映射到NavDestination各业务页面通过pushPath()发起导航通过pop()返回。项目现有两种带参路径详情页传JSON.stringify(item)筛选页传纪念日类型字符串。本文以当前仓库中的 ArkTS 源码为事实边界先复核这条已经写入代码的导航链路再把重点放在参数契约为什么路由名集中后仍不等于类型安全完整实体 JSON 为什么可能过期筛选参数如何建立白名单未知路由和空栈返回如何兜底以及怎样在不推翻现有结构的前提下引入类型化导航封装。本文将解决根Navigation、共享栈与appRouter如何连接。Tab 切换与二级页面入栈为什么不能混为一谈。RouteNames能防止哪些错误不能防止哪些错误。详情 JSON 参数与筛选字符串的真实风险。如何设计类型化路由请求、参数守卫和未知路由兜底。如何验证系统返回、连续入栈、删除后返回和窗口恢复。本文唯一标记CSDN-SERIES:ALL-163208646一、根导航只在 Index 绑定一次真实入口页Entry Component struct Index { StorageLink(StateKeys.NAV_STACK) pathStack: NavPathStack new NavPathStack(); StorageLink(StateKeys.THEME_BG) themeBg: string #F5F0E8; build() { Navigation(this.pathStack) { MainTabShell() } .navDestination(appRouter) .hideTitleBar(true) .hideToolBar(true) .mode(NavigationMode.Stack) .backgroundColor(this.themeBg); } }NavigationMode.Stack让二级页面覆盖主 Tab 壳appRouter统一解释入栈名称pathStack是所有页面操作的同一个共享栈。入口页不直接写每个业务目的地也不处理详情参数。这样新增路由主要修改RouteNames和RouteMap根页面保持稳定。二、共享 NavPathStack 的创建时机AppStore.bootstrap()只在键未定义时创建if (AppStorage.getNavPathStack(StateKeys.NAV_STACK) undefined) { AppStorage.setOrCreateNavPathStack( StateKeys.NAV_STACK, new NavPathStack() ); }业务页面再用相同键链接StorageLink(StateKeys.NAV_STACK) pathStack: NavPathStack new NavPathStack();字段后的new NavPathStack()是声明时默认值正常启动链已由 bootstrap 提供共享对象。关键是不在页面出现时主动覆盖AppStorage中的栈。错误做法后果每页持有独立栈push 后根 Navigation 无变化配置变化时重建全局栈当前详情和返回历史丢失用字符串模拟栈层级系统返回与 UI 不一致把业务实体长期存在栈中数据更新后参数过期导航栈属于应用级基础设施但页面仍只应通过明确方法表达导航意图。历史证据哪些结论能证明哪些仍不能证明这次审计先查了项目源码和现有错误记录。当前能直接证明的事实是根Navigation、共享NavPathStack、集中式RouteNames、appRouter映射以及多个页面中的pushPath()、pop()调用都真实存在。它们共同说明项目已经选择“一个主栈承载二级页面”的结构而不是每个页面各建一套导航容器。错误记录中没有找到一条带日期、带复现步骤的“主导航故障已经修复”记录。仓库历史中出现过assembleHap成功证据但对应的是其他功能修改不能挪用为本文导航方案的构建证明。项目规则里还记录过“返回页面时不能只依赖aboutToAppear刷新数据”这能支持“导航与数据刷新需要分离”的设计判断却不能证明本文列出的系统返回、异常参数、进程恢复等场景已经执行过测试。因此后文把内容分为三类第一类是可以从当前源码逐项定位的事实第二类是从旧记录得到的有限历史证据第三类是尚未落地的建议实现。文中不会把建议代码写成现有能力也不会把过去某次无关构建成功描述为本次导航改造已经通过。读者在自己的项目里采用建议后仍需要重新编译并执行对应的导航测试矩阵。三、RouteNames先消灭散落的魔法字符串真实常量export class RouteNames { static readonly MAIN main; static readonly DETAIL detail; static readonly COUPLE couple; static readonly DIARY diary; static readonly HABIT habit; static readonly HABIT_WALL habitWall; static readonly WISH_LIST wishList; static readonly ALBUM album; static readonly FILTERED_LIST filteredList; static readonly THEME_SETTINGS themeSettings; static readonly MOOD_SETTINGS moodSettings; static readonly PRIVACY_POLICY privacyPolicy; }常量能防止detail和details这类拼写漂移也便于全局搜索。但它只约束路由名不能约束该路由是否必须带参数。参数是什么类型。参数字段是否完整。当前栈是否允许进入该页面。因此RouteNames是统一协议的第一层不是完整类型系统。四、RouteMap目的地集中构建appRouter根据名称构造页面Builder export function appRouter(name: string, param: Object) { NavDestination() { if (name RouteNames.DETAIL) { DetailView({ itemParam: param as string }); } else if (name RouteNames.COUPLE) { CoupleView(); } else if (name RouteNames.FILTERED_LIST) { FilteredListView({ filterType: param as string }); } } .hideTitleBar(true) .padding({ bottom: px2vp( AppStorage.getnumber(StateKeys.SAFE_AREA_BOTTOM) ?? 0 ) }); }这里还统一隐藏目的地标题栏并添加底部安全区。页面不必重复构造NavDestination外壳。当前分支没有未知路由页面。若 name 不匹配NavDestination内容为空可能表现为白页。生产环境应提供日志和安全兜底。源码审计现有集中路由仍有四个明确边界第一RouteNames只集中名称没有把“路由名”和“参数类型”绑定起来。调用方写对了detail仍可能漏传参数、传入筛选字符串或把另一个对象强制断言成详情参数。集中常量解决的是拼写一致性不是请求合法性。第二param as string是编译期断言不是运行时校验。appRouter没有先判断param是否真的是字符串也没有判断筛选值是否属于业务白名单。只要调用点绕过约束目的地就会收到不符合预期的值。筛选页对未知值使用兜底标题再按相等条件过滤数据结果可能是“标题看起来正常但列表为空”这比直接报错更难定位。第三详情页解析失败时使用了空的catch。当前组件已经有一个默认Anniversary对象坏字符串不会必然导致页面崩溃却可能让页面继续展示默认日期、空标题或不对应真实记录的状态。这里真正的风险不是只有异常退出还包括“失败被吞掉后页面看似可用”。参数解析失败应进入明确的错误状态不能把默认业务对象当作有效数据。第四未知name没有兜底分支。NavDestination外壳仍会创建但内部没有业务页面用户可能看到空白内容。当前源码也没有针对未知名称的诊断信息。一个可维护的路由层需要把这种失败变成可观察、可返回的状态同时避免记录完整业务参数。完整对象 JSON 还有一层数据一致性问题入栈参数代表当时的快照不是仓库真源。事项被编辑或删除后旧栈中的 JSON 不会自行变化如果未来参数参与状态恢复字段演进也会增加兼容负担。因此本文后面的“只传 ID”属于建议方案不是对当前实现的描述。排查这四类边界时可以先沿调用链逐层缩小范围入口是否使用共享栈路由名称是否存在参数是否通过守卫目的地是否成功构建返回是否操作同一栈。每一层只记录必要状态不打印完整实体。这样即使最终现象都是“页面没有正常出现”也能区分是入口、协议、参数、页面构建还是返回历史的问题避免用增加延时或重复跳转掩盖根因。五、主 Tab 与二级路由是两套状态MainTabShell用Tabs管理首页、全部、新建、组件和我的它通过currentIndex切换。详情、情侣空间、日记、设置等使用NavPathStack入栈。Tab 层 首页 | 全部 | 新建 | 组件 | 我的 Navigation 栈 MainTabShell - FilteredList - Detail如果把 Tab 切换也写成pushPath连续切换会堆积多个主页面如果用 Tab 索引打开详情又无法获得自然的系统返回。真实源码把两层分开这是合理的。返回二级页时pop()不会改变currentIndex用户会回到发起导航的原 Tab。六、无参数入口页面只需要路由名首页快捷入口this.pathStack.pushPath({ name: RouteNames.DIARY }); this.pathStack.pushPath({ name: RouteNames.COUPLE }); this.pathStack.pushPath({ name: RouteNames.WISH_LIST });这些页面不需要定位具体实体路由请求只包含名称。ProfileView 也复用同一常量进入主题、心情与隐私页面。入口来源不同目的地协议一致。无参数不等于任何调用都允许附带任意 param。统一封装可以主动拒绝多余参数避免未来路由语义模糊。七、筛选页参数裸字符串需要白名单首页传入this.pathStack.pushPath({ name: RouteNames.FILTERED_LIST, param: countdown });还会传memorial、love和salary。RouteMap直接FilteredListView({ filterType: param as string });类型断言只告诉编译器“把它当字符串”不会验证值。若传入salaryy筛选页可能得到空列表或错误标题。可以复用业务联合类型export type FilterRouteType | countdown | memorial | love | salary | custom; function isFilterRouteType(value: Object): value is FilterRouteType { return value countdown || value memorial || value love || value salary || value custom; }路由 Builder 在创建页面前检查不合法时记录日志并展示可返回的错误状态。八、详情参数完整 JSON 能用但会携带旧快照首页和列表都这样进入详情this.pathStack.pushPath({ name: RouteNames.DETAIL, param: JSON.stringify(item) });详情页出现时解析if (this.itemParam.length 0) { try { this.item JSON.parse( this.itemParam ) as Anniversary; } catch (_) { } }好处是详情首帧不必再次读仓库问题是参数只是入栈时的快照。其他页面修改或删除同一事项后栈里的 JSON 不会自动更新。as Anniversary同样不验证字段结构。更稳的路由只传 IDexport interface DetailRouteParam { id: string; } this.pathStack.pushPath({ name: RouteNames.DETAIL, param: { id: item.id } as DetailRouteParam });详情页按 ID 从仓库读取最新对象并处理“不存在或已删除”状态。九、JSON.parse 成功不等于参数合法下面的字符串能成功解析{id:123,title:[]}但它不满足真实Anniversary。应使用字段守卫function isDetailRouteParam( value: Object ): value is DetailRouteParam { const candidate value as Recordstring, Object; return typeof candidate.id string candidate.id.length 0; }ArkTS 对索引签名和宽泛对象有更严格限制具体写法应按项目 SDK 编译规则调整。原则不变先检查再构建页面断言不是校验。十、类型化导航封装让调用点不能传错可以定义路由请求联合类型export type AppRouteRequest | { name: typeof RouteNames.DETAIL; param: DetailRouteParam; } | { name: typeof RouteNames.FILTERED_LIST; param: FilterRouteType; } | { name: typeof RouteNames.DIARY } | { name: typeof RouteNames.COUPLE };再包装导航export class AppNavigator { constructor(private readonly stack: NavPathStack) {} push(request: AppRouteRequest): void { this.stack.pushPath(request); } back(): void { this.stack.pop(); } }调用详情却漏掉param或给 Diary 传筛选字符串时编译阶段就能发现。封装不需要接管整个 ArkUI Navigation只需守住应用协议。分阶段改造先让失败可见再收紧参数不建议一次性重写所有页面。第一阶段只建立边界为详情、筛选和无参页面定义明确的请求类型在appRouter入口检查名称和参数解析失败时进入错误目的地。这个阶段仍可兼容旧的详情 JSON 字符串但必须把“解析失败”和“字段不合法”变成可见状态。验收点是旧入口继续可用坏参数不再静默生成默认业务对象。第二阶段集中调用方法。把散落的pushPath({ name, param })收拢为openDetail(id)、openFilteredList(type)、openDiary()等窄接口并在接口内部创建请求。页面只表达“我要打开哪个业务目标”不再知道底层参数包装形式。迁移时可逐页替换每替换一个入口就搜索旧写法避免新旧协议长期并存。第三阶段把详情参数从完整实体改成稳定 ID。详情页收到 ID 后向仓库查询最新数据并显式处理加载中、查询失败和记录不存在。删除记录后返回来源列表来源页根据仓库或版本信号刷新而不是依赖路由回传一份新数组。这个阶段需要确认仓库接口、页面状态和生命周期不能只改一行param就宣称完成。第四阶段补齐观察与恢复。未知路由显示可返回页面日志只记录路由名、参数类别和校验结果重复点击入口时确认是否允许连续入栈配置变化或状态恢复时检查栈中 ID 是否仍对应有效记录。每一阶段都应单独编译、运行和回归失败时可以回到上一阶段定位而不是把类型、仓库和 UI 三类问题混在一次大改中。这套顺序的核心是先消除静默失败再改善类型体验最后调整数据读取。若项目当前没有仓库按 ID 查询能力先保留旧 JSON 兼容分支也比直接强转更稳妥。本文的示例只描述建议边界是否能通过当前 ArkTS 编译器仍应由实际 SDK 和项目构建结果决定。十一、返回路径pop 之前先理解栈语义真实二级页面普遍使用.onClick(() { this.pathStack.pop(); })详情删除成功后也pop()返回列表。系统返回与可见返回按钮都应作用于同一栈避免一个改变页面状态另一个退出 Ability。需要覆盖的边界栈只有根页面时不应盲目 pop。保存弹层打开时返回应先处理弹层或草稿确认。删除实体后返回来源列表要重新加载。连续详情入栈时一次返回只退一层。可以包装安全返回function backOrStay( stack: NavPathStack ): boolean { const paths stack.getAllPathName(); if (paths.length 0) { return false; } stack.pop(); return true; }具体获取栈信息的 API 以当前 SDK 为准不要凭记忆使用不存在的方法。十二、未知路由不能静默生成空页面当前RouteMap最后一项没有else。演进时可以增加统一错误目的地} else { RouteErrorView({ routeName: name, onBack: () { const stack AppStorage.getNavPathStack( StateKeys.NAV_STACK ); stack?.pop(); } }); }错误页至少应包含无法打开页面的短提示。返回按钮。不含敏感参数的路由名日志。不自动循环重试。线上用户遇到未知路由时可恢复比空白页更重要。十三、安全区统一在 NavDestination 外壳处理RouteMap为所有二级页添加底部 padding.padding({ bottom: px2vp( AppStorage.getnumber( StateKeys.SAFE_AREA_BOTTOM ) ?? 0 ) });统一外壳减少页面遗漏但要防止业务页面自身又添加相同安全区造成双倍空白。应定义清楚二级页的系统底部避让由路由壳负责页面只处理内容间距或者改成页面根组件统一处理二者择一。如果安全区在窗口变化时更新直接在 Builder 中调用AppStorage.get()是否触发重建也要验证。更响应式的方式是由宿主通过StorageProp读取后传入。十四、导航参数不要携带敏感正文当前详情参数序列化整个Anniversary可能包含备注、封面等字段。它仍在本地内存中但调试日志、错误上报或未来状态恢复若打印路由参数可能扩大敏感信息范围。只传 ID 有三项收益减少栈参数体积。避免旧快照。降低日志泄露正文的风险。日志可以记录路由名和实体 ID 的脱敏片段不应打印用户备注、日记或情侣留言。工程上可以给导航日志设一条明确边界允许输出routeName、参数类型、校验结果和截断后的实体 ID禁止输出序列化后的业务对象。这样既能定位“哪条路由参数不合法”又不会把纪念日备注或图片路径带进日志。hilog.info(0x0000, Router, route%{public}s, id%{public}s, RouteNames.DETAIL, anniversaryId.slice(0, 8))这里的日志只服务于路由定位。若 ID 本身也具有业务敏感性应改为不可逆摘要或完全不记录导航模块不应因为调试方便而突破数据最小化原则。十五、导航与数据刷新协作新建事项保存后递增DATA_VERSION首页和列表可重新读取仓库。导航只负责回到来源页面不负责携带更新后的完整数组。AddView 保存 - Repository 写入 - DATA_VERSION 1 - 相关页面重读 Detail 删除 - Repository 删除 - pop - 来源页面刷新把“导航完成”和“数据已更新”分开可以避免通过复杂返回参数同步列表。路由传定位信息仓库保存真数据失效信号触发重读。十六、导航测试矩阵当前未验证项本次源码审计没有执行新的assembleHap也没有在模拟器或真机上注入非法路由。系统返回手势、连续快速点击、详情中旋转或切换主题、进程被回收后的路径恢复、记录删除后的旧 ID、未知名称以及根栈返回都属于待验证项。下面表格给出的是实施改造后的验收清单不是已经取得的测试结果。验证时应保留最小证据使用的构建命令及退出码、设备或模拟器环境、入口路径、输入参数、实际页面和返回结果。只看到“点击后出现详情页”不能覆盖错误参数与返回链路只看到构建成功也不能代替运行时导航验证。如果某项因设备、签名或 SDK 环境无法执行应把它标记为未运行并记录原因不应推断为通过。场景操作期望无参入口首页进日记正确创建页面筛选入口传love只显示恋爱类非法筛选传未知字符串拦截并可返回详情入口传有效 ID读取最新实体详情缺失ID 已删除显示不存在状态JSON 损坏旧路径传坏字符串不崩溃连续入栈列表进详情再进设置返回逐层恢复可见返回点击左上角pop 一层系统返回系统手势与可见返回一致根栈返回主 Tab 按返回不产生空白页旋转/主题详情中切换配置栈保持删除返回删除详情项来源列表刷新导航测试不能只点一次入口。至少覆盖非法参数、第二次进入和返回后刷新。十七、常见问题与修复现象根因修复点击入口无反应页面使用了独立栈共享NAV_STACK跳转后白页未知路由无兜底增加错误目的地筛选页标题异常裸字符串未校验使用联合类型白名单详情展示旧数据参数传完整实体快照只传 ID 并重读JSON 解析后仍崩溃类型断言代替校验增加结构守卫返回退出应用页面与根栈不一致统一 pop 语义底部空白过大安全区重复添加明确单一所有者配置变化丢详情重建全局栈bootstrap 只创建一次定位导航问题时记录“当前路由名、参数验证结果、栈深度、来源页面”不要打印完整业务正文。十八、发布前核对[ ]Index绑定共享 NavPathStack 和 appRouter。[ ] 路由名全部来自RouteNames。[ ] RouteMap 覆盖所有公开二级入口。[ ] 未知路由有日志和可返回页面。[ ] 筛选类型经过白名单校验。[ ] 详情优先传 ID而不是长期保存实体快照。[ ] JSON 解析后执行结构验证。[ ] 可见返回和系统返回作用于同一栈。[ ] 根页面返回不会生成空白 NavDestination。[ ] 删除、保存后来源页面读取仓库真源。[ ] 安全区只由一个层级负责。[ ] 路由日志不包含用户私密正文。十九、总结导航的核心是协议不是跳转 API时光清单已经具备统一主链AppStorage 持有根NavPathStackIndex构建单一NavigationRouteNames集中名称RouteMap集中目的地各页面统一pushPath和pop。Tab 状态与二级栈分开入口和返回路径清晰。下一步最重要的改造不是换一种跳转动画而是强化参数协议。当前详情传完整 JSON 字符串筛选页传裸类型字符串二者都依赖运行时断言。通过类型化路由请求、白名单守卫、只传实体 ID、未知路由兜底和返回测试可以把“能跳”升级为“可验证、可恢复、可演进”。这才是主导航长期稳定的基础。AI 辅助声明本文由 AI 辅助整理当前导航链路均依据AppStore.ets、Index.ets、RouteNames.ets、RouteMap.ets、HomeView.ets、AllView.ets、FilteredListView.ets与DetailView.ets的真实源码人工复核类型化参数与错误目的地为明确标注的演进建议。
返回列表