【寻迹校园 HarmonyOS NEXT 实战 03】NavPathStack 路由实战:18 个页面如何集中管理

【寻迹校园 HarmonyOS NEXT 实战 03】NavPathStack 路由实战:18 个页面如何集中管理

这是"寻迹校园 HarmonyOS NEXT 实战"系列第 3 篇。本文结合 ArkUI Stage Model 项目,拆解稳定路由键、类型化参数、统一 destinationBuilder,以及同一个 NavPathStack 如何复用到手机与大屏 Shell。

上图为本文原创生成的集中路由概念图:稳定路由键从中心导航枢纽进入统一页面栈,并可沿受控分支返回。它用于解释导航关系,不代表实际页面视觉。

一、路由问题通常不是"跳不过去",而是失去上下文

"寻迹校园"包含首页、发布类型、两类发布表单、编辑、发布成功、匹配、详情、小艺、认领申请、认领审核、交接、举报、举报进度、消息、个人中心、我的发布和设置等路由。

如果每个页面都直接写字符串并传裸对象,项目很快会出现:

  • match_resultsmatch-result 等字符串不一致;
  • 详情页不知道用户从哪条查询记录进入;
  • 编辑页丢失报告类型;
  • 大屏嵌入详情和手机独立详情使用两套参数;
  • Tab 切换与二级页面 push 混在一起;
  • 返回后数据刷新依赖页面之间相互修改状态。

因此路由治理的目标不是减少 pushPathByName() 次数,而是让"路由键、参数、页面组装、返回和刷新"形成统一契约。

二、用 AppRoute 集中稳定路由键

项目把所有内部路由键放到 common-core

ts 复制代码
export class AppRoute {
  static readonly HOME: string = 'home';
  static readonly PUBLISH_TYPE: string = 'publish_type';
  static readonly PUBLISH_LOST_FORM: string = 'publish_lost_form';
  static readonly PUBLISH_FOUND_FORM: string = 'publish_found_form';
  static readonly REPORT_EDIT: string = 'report_edit';
  static readonly PUBLISH_SUCCESS: string = 'publish_success';
  static readonly MATCH_RESULTS: string = 'match_results';
  static readonly XIAOYI_AGENT: string = 'xiaoyi_agent';
  static readonly ITEM_DETAIL: string = 'item_detail';
  static readonly CLAIM_REQUEST: string = 'claim_request';
  static readonly CLAIM_REVIEW: string = 'claim_review';
  static readonly HANDOFF: string = 'handoff';
  static readonly MODERATION_REPORT: string = 'moderation_report';
  static readonly MODERATION_PROGRESS: string = 'moderation_progress';
  static readonly MESSAGES: string = 'messages';
  static readonly PROFILE: string = 'profile';
  static readonly MY_REPORTS: string = 'my_reports';
  static readonly SETTINGS: string = 'settings';
}

显示文案可以变化,内部 key 不应因为"匹配结果"改成"智能匹配"就随意变化。稳定 key 对返回栈、状态恢复和未来深链都更安全。

三、路由参数不要使用匿名裸对象

仅集中路由字符串还不够。参数如果写成 { id: report.id, from: current.id },调用方和目标页仍然无法共享类型契约。

图中的数据胶囊代表类型化路由参数:它沿统一页面栈前进,返回时则按栈顺序逐层退出。页面只接收完成导航所需的最小参数,数据更新仍由权威数据源重新查询。

项目为主要路由定义参数类:

ts 复制代码
export class ReportDetailRouteParam {
  reportId: string = '';
  sourceReportId: string = '';

  constructor(reportId: string, sourceReportId: string = '') {
    this.reportId = reportId;
    this.sourceReportId = sourceReportId;
  }
}

export class ReportEditRouteParam {
  reportId: string = '';
  reportType: ReportType = ReportType.LOST;

  constructor(reportId: string, reportType: ReportType) {
    this.reportId = reportId;
    this.reportType = reportType;
  }
}

export class MatchRouteParam {
  queryReportId: string = '';

  constructor(queryReportId: string) {
    this.queryReportId = queryReportId;
  }
}

sourceReportId 很重要。用户从一条丢失记录查看某条拾得候选,再提交认领申请时,需要同时保留"目标拾得记录"和"来源丢失记录"。最终双方完成交接后,Service 才能把两条关联记录一起转为 RESOLVED

四、一个 destinationBuilder 负责页面组装

Index.ets 持有一个应用级 NavPathStack

ts 复制代码
@Entry
@ComponentV2
struct Index {
  private readonly pathStack: NavPathStack = new NavPathStack();
  @Local selectedTab: BottomTab = BottomTab.HOME;
  @Local dataRevision: number = 0;
  @Local screenWidth: number = 0;
}

所有业务目标页由同一个 Builder 映射:

ts 复制代码
@Builder
private destinationBuilder(name: string, param: object) {
  NavDestination() {
    if (name === AppRoute.PUBLISH_SUCCESS) {
      PublishSuccessPage({
        pathStack: this.pathStack,
        reportId: (param as PublishSuccessRouteParam).reportId
      })
    } else if (name === AppRoute.MATCH_RESULTS) {
      MatchResultsPage({
        pathStack: this.pathStack,
        queryReportId: (param as MatchRouteParam).queryReportId
      })
    } else if (name === AppRoute.ITEM_DETAIL) {
      ItemDetailPage({
        pathStack: this.pathStack,
        reportId: (param as ReportDetailRouteParam).reportId,
        sourceReportId: (param as ReportDetailRouteParam).sourceReportId
      })
    }
  }
  .hideTitleBar(true)
  .backgroundColor(AppColors.PAGE_BACKGROUND)
}

这样做的收益有三点:

  1. 页面创建入口只有一个,参数读取位置清晰;
  2. 手机、平板和大屏 Navigation 都复用相同映射;
  3. 页面需要刷新外部数据时,通过回调触发,而不是自己修改根页面状态。

五、main_pages.json 与业务路由不是一回事

当前 main_pages.json 只登记两个入口:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/XiaoYiFunctionPage"
  ]
}

Index 是主 ArkUI 页面壳。绝大多数业务页面通过 NavDestination 组装,不需要都写进 main_pages.json

XiaoYiFunctionPage 是独立 @Entry 页面,因为它使用系统 Agent Framework Kit 的 FunctionComponent,由宿主页通过 Router 打开。这是平台能力页面与普通业务导航的边界,不应把两套路由机制混为一谈。

六、Tab 切换与页面 push 要分开

底部导航有首页、发布、消息、我的四个入口,但"发布"不是一个常驻 Tab 内容,而是一个业务动作:

ts 复制代码
private selectTab(tab: BottomTab): void {
  if (tab === BottomTab.PUBLISH) {
    this.pathStack.pushPathByName(AppRoute.PUBLISH_TYPE, undefined);
    return;
  }
  this.selectedTab = tab;
}

首页、消息和我的保留 Tab 语义;点击发布则进入发布类型选择页。这样用户返回后仍处于原来的 Tab,不会多出一个空的"发布 Tab 根页面"。

项目按宽度使用不同导航外壳:

  • < 600vp:手机底部导航;
  • 600--839vp:顶部导航;
  • >= 840vp:常驻侧边导航;
  • >= 1280vp:内容区内部升级为三栏工作台。

无论 Shell 如何变化,业务页仍使用同一个 pathStack 和同一个 destinationBuilder

ts 复制代码
Navigation(this.pathStack) {
  this.tabContent()
}
.hideTitleBar(true)
.mode(NavigationMode.Stack)
.navDestination(this.destinationBuilder)

这能避免窗口从 lg 切到 xl 时创建新的导航栈。布局可以变化,用户当前路径和业务上下文不应随断点重建。

八、返回后的数据刷新不要靠路由参数回传整份对象

发布、认领、交接和举报完成后,页面通过回调递增 dataRevision

ts 复制代码
ClaimReviewPage({
  pathStack: this.pathStack,
  claimId: (param as ClaimReviewRouteParam).claimId,
  onDataChanged: () => this.dataRevision++
})

dataRevision 只是"数据已失效"的信号。首页、消息和我的发布收到新版本后,再从 Service 读取 canonical data,而不是由子页面把新对象逐层传回,更不会手工修改多个页面的计数。

这适合当前单机规模。未来如果引入复杂 ViewModel、远端同步或多个窗口,可以升级为更完整的状态容器,但核心原则不变:刷新信号不拥有业务数据。

九、路由验收应该检查什么

路由验证不能只看"点击后页面出现"。至少要覆盖:

  • 空参数和错误 ID 是否显示用户可理解的错误;
  • 手机独立详情与大屏嵌入详情是否共享同一 reportId;
  • 从候选进入详情后 sourceReportId 是否保留;
  • 发布成功返回后首页和我的发布是否刷新;
  • 二级页面返回是否回到正确 Tab;
  • md/lg/xl 断点往返时路径和选中态是否保持;
  • 小艺独立 Entry 页面返回后是否回到原生候选。

十、本文小结

集中路由治理包含四件事:稳定的 AppRoute、类型化参数、统一 destinationBuilder 和单一 NavPathStack。在此基础上,手机底栏、平板顶栏和大屏侧栏只是不同的外壳,不会复制业务导航。

下一篇将讨论页面和 Service 之间的统一结果契约:如何用 OperationResult<T> 收敛校验失败、存储异常和系统平台错误。

系列导航:第 3 篇 / 共 50 篇。上一篇:《多模块工程拆解》;下一篇:《ArkTS 统一结果模型 OperationResult》。

相关推荐
m0_749690232 小时前
【寻迹校园 HarmonyOS NEXT 实战 02】多模块工程拆解:entry、HAR 与 HSP 如何分工
华为·harmonyos·arkts·软件架构·har·hsp
用户0934077735143 小时前
HarmonyOS WPS Open SDK 实践:用 SdkConstants 判断当前 HAR 形态
harmonyos
独守一片天4 小时前
HarmonyOS鸿蒙新生态智能体意图框架怎么设计?
华为·harmonyos
梦想不只是梦与想16 小时前
鸿蒙应用api的兼容性:参数配置(二)
harmonyos·sdk版本·sdkversion
北墨NoLimit21 小时前
鸿蒙线程间通信怎么选:TaskPool、TaskGroup、LongTask 与 Worker 实战
typescript·harmonyos
woshihuanglaoshi1 天前
错题四科入库:鸿蒙错题本种子数据与复习队列效果
学习·华为·harmonyos·鸿蒙
kiros_wang1 天前
鸿蒙ArkTS枚举实战|静态枚举、动态枚举业务选型、规范落地与避坑全解
harmonyos
2501_919749031 天前
华为鸿蒙免费音乐APP—小羊免费音乐
华为·harmonyos·鸿蒙
世人万千丶1 天前
物品借还闭环:鸿蒙物品清单种子数据与清单效果
学习·华为·harmonyos·鸿蒙