【天体运行模拟|14】HarmonyOS ArkTS 主导航实战:统一页面入口、返回路径和参数校验

主导航最容易出现的不是“点不动”,而是三套入口各自为政:首页按钮直接 pushUrl,热门实验卡片又拼一遍参数,Tab 切换依赖裸数字,二级页面返回时只能假设栈顶正确。页面越多,路径拼写、参数缺失、默认场景误跳和返回层级异常就越难复核。

“天体运行模拟”的真实首屏由 Index.ets 组装:首页、关卡、知识、我的四个底部 Tab;HomePage.ets 既能通过回调切换到关卡 Tab,也能直接打开稳定双体、自由宇宙和热门实验。本文基于这两份真实源码,面向 HarmonyOS 5.0 及以上版本,保留现有轻量结构,同时用类型化入口、统一路由契约和参数校验把主导航写稳。

主导航工程封面

本文会处理:

  • 四个 Tab 的索引如何从裸数字变成稳定契约;
  • Tab 内切换与压入二级页面为何要区分;
  • 实验页的 expIdexpName 如何统一构造与校验;
  • 返回动作如何保持可预测,不制造重复首页;
  • phone、tablet、2in1 下底部导航如何稳定适配。

项目基线:应用版本 1.0.0targetSdkVersion 6.0.2(22)compatibleSdkVersion 6.0.1(21),设备类型为 phone、tablet 与 2in1。文中“统一导航”是工程演进方案,现有源码仍直接使用 TabsControllerrouter.pushUrl()

一、真实主导航是四个 Tab

Index.ets 维护当前索引和 TabsController

@State currentIndex: number = 0
private tabController: TabsController =
  new TabsController()

四个 TabContent 分别装载:

TabContent() { HomePage() }
TabContent() { LabPage() }
TabContent() { LearningPage() }
TabContent() { MinePage() }

这是一种产品级主导航:Tab 负责一层功能域,二级详情页再使用 Router 压栈。不能把所有页面都塞成 Tab,也不能用 Router 重复创建首页来模拟 Tab 切换。

二、首页切换 Tab 的真实方式

HomePage 接收一个回调:

export struct HomePage {
  onSwitchTab:
    (index: number) => void = () => {}
}

Index 注入:

HomePage({
  onSwitchTab: (index: number) => {
    this.tabController.changeIndex(index)
  }
})

首页点击“关卡模式 >”时调用 onSwitchTab(1)。这条链路不会创建新页面栈,只改变当前 Tab,符合一级导航语义。

三、裸数字索引是第一个风险

0123 对编译器没有业务含义。Tab 顺序调整后,散落的 onSwitchTab(1) 可能跳错页面。

export enum MainTab {
  HOME = 0,
  LAB = 1,
  LEARNING = 2,
  MINE = 3
}

调用改为:

this.onSwitchTab(MainTab.LAB)

枚举不会增加运行复杂度,却让页面入口可以搜索、重构和测试。

四、主 Tab 与二级路由必须分流

首页包含两类动作:

// 一级导航
this.onSwitchTab(1)
// 二级页面
router.pushUrl({
  url: 'views/experiment/ExperimentSimPage',
  params: { expId: 'stable_orbit' }
})

一级功能域切换使用 TabsController;需要独立返回路径的详情、编辑器和模拟页使用 Router。两者混用会出现返回键回到重复首页、Tab 状态丢失等问题。

主导航与二级路由流程

五、先建立页面路径常量

当前字符串路径在多个页面重复。建议集中定义:

export const AppRoutes = {
  EXPERIMENT_SIM:
    'views/experiment/ExperimentSimPage',
  EXPERIMENT_RESULT:
    'views/experiment/ExperimentResultPage',
  KNOWLEDGE_DETAIL:
    'views/learning/KnowledgeDetailPage',
  FAVORITES:
    'views/mine/FavoritesPage'
} as const

路径常量仍需与 main_pages.json 一致。可以在构建脚本中验证每个常量都存在于清单,避免运行时才发现拼写错误。

六、实验入口需要统一参数模型

真实首页有三种实验入口:

  1. “开始模拟”传 stable_orbit
  2. “自由宇宙”传 sandbox
  3. 热门实验卡传 exp.idexp.name

先定义参数:

export interface ExperimentRouteParams {
  expId: string
  expName?: string
}

再由一个方法构造:

function openExperiment(
  params: ExperimentRouteParams
): void {
  router.pushUrl({
    url: AppRoutes.EXPERIMENT_SIM,
    params
  })
}

入口不再自行拼 URL,后续增加来源、预设或埋点时只改一个边界。

七、expName 不是权威身份

实验 ID 是稳定身份,名称是展示字段。接收页应当用 expId 从实验目录解析当前名称,而不是无条件相信路由传来的 expName

const exp = getAllExperiments()
  .find(item => item.id === params.expId)
if (!exp) {
  this.pageState = 'notFound'
  return
}
this.expId = exp.id
this.title = exp.name

这样应用升级后实验改名,旧入口仍显示最新标题;恶意或错误参数也无法伪造场景名称。

八、参数校验不能依赖类型断言

router.getParams() as ExperimentRouteParams 不会校验运行时数据。更稳的解析器:

function parseExperimentParams(
  raw: object | undefined
): ExperimentRouteParams | undefined {
  if (!raw) return undefined
  const value = raw as Record<string, unknown>
  if (typeof value.expId !== 'string' ||
      value.expId.trim().length === 0) {
    return undefined
  }
  return {
    expId: value.expId,
    expName: typeof value.expName === 'string'
      ? value.expName : undefined
  }
}

解析失败应进入错误或缺省状态。只有入口产品明确允许默认场景时,才能回退到 stable_orbit

九、默认场景必须是显式产品决策

当前模拟页缺少 expId 时会使用稳定双体默认值。这对首页“开始模拟”合理,但对“查看相关实验”或收藏详情可能掩盖参数丢失。

可以增加来源:

export type ExperimentEntrySource =
  | 'home_primary'
  | 'home_hot'
  | 'lab'
  | 'favorite'
  | 'knowledge'

只有 home_primary 允许缺省稳定双体;其他来源缺参就显示不可用。这样按钮文案和实际目标保持一致。

十、返回路径用 router.back,不重复 push 首页

二级页面的返回动作应优先:

router.back()

不要用:

router.pushUrl({ url: 'pages/Index' })

后者会在栈上再创建一个首页,连续操作后返回路径越来越长。主 Tab 页面本身通常由系统返回行为退出应用或回到上一个任务,不需要自建返回按钮。

十一、首页热门卡片的真实入口

首页从实验目录截取前 4 或 6 个:

private hotExperiments(): Experiment[] {
  return this.experiments.slice(
    0,
    this.screenWidth > 600 ? 6 : 4
  )
}

点击卡片传真实模型:

router.pushUrl({
  url: 'views/experiment/ExperimentSimPage',
  params: {
    expId: exp.id,
    expName: exp.name
  }
})

这里不是推荐算法,而是目录顺序截取。文章和产品文案不应宣称“智能推荐”。

十二、统一导航服务应保持窄职责

不需要构造庞大的全局路由框架。一个窄服务足够:

export class AppNavigator {
  static openExperiment(
    expId: string,
    source: ExperimentEntrySource
  ): void {
    const exp = getAllExperiments()
      .find(item => item.id === expId)
    if (!exp) return
    router.pushUrl({
      url: AppRoutes.EXPERIMENT_SIM,
      params: {
        expId: exp.id,
        expName: exp.name,
        source
      }
    })
  }
}

它负责路径、参数和目录验证,不负责页面业务状态,也不持有组件实例。

主导航四层职责

十三、Tab 状态如何保持

currentIndexIndex@State

.onChange((index: number) => {
  this.currentIndex = index
})

只要 Index 没被重复创建,从二级页面返回后 Tab 仍然存在。若需要进程重启后恢复最后 Tab,可以保存一个轻量整数,但必须校验范围:

function normalizeTab(index: number): MainTab {
  if (index < MainTab.HOME ||
      index > MainTab.MINE) {
    return MainTab.HOME
  }
  return index as MainTab
}

是否恢复最后 Tab 是产品决策。面向快速开始的工具,冷启动回首页也很合理。

十四、重复点击要防止重复压栈

按钮快速连点可能连续执行 pushUrl()。可以增加短暂导航锁:

private navigating: boolean = false
private async openOnce(
  action: () => Promise<void>
): Promise<void> {
  if (this.navigating) return
  this.navigating = true
  try {
    await action()
  } finally {
    setTimeout(() => {
      this.navigating = false
    }, 300)
  }
}

更重要的是在真机上验证 Router Promise 和页面动画行为,不要用过长锁让正常返回后无法再次进入。

十五、导航失败要有页面反馈

入口路径错误、参数无效或目标不存在时,不能只写日志。首页可以展示轻量提示;详情页可以显示 notFound

type RouteState =
  | 'ready'
  | 'navigating'
  | 'invalid'
  | 'failed'
@State routeState: RouteState = 'ready'

导航失败后恢复按钮可点击状态,并给用户明确文案,例如“该实验暂不可用”,而不是静默无响应。

十六、底部 Tab 的视觉状态

真实 TabBarBuilder 根据当前索引切换图标与文字颜色:

Image(
  this.currentIndex === targetIndex
    ? iconSelected
    : iconNormal
)
.fillColor(
  this.currentIndex === targetIndex
    ? AppColors.TAB_SELECTED
    : AppColors.TAB_UNSELECTED
)

项目目前传入的 normal 与 selected 图标资源相同,主要依靠填充色区分。发布前应确认图标支持着色,并验证选中/未选中对比度不只靠细微色差。

十七、Tab 尺寸与系统避让

源码设置:

.barHeight(56)
.padding({
  top: this.statusBarHeight,
  bottom: this.bottomBarHeight
})

56vp 是稳定的主导航高度。若改为沉浸式布局,底部避让必须计算系统导航区,不能只依赖固定 0。按钮命中区域应覆盖整格,而不是只有 24vp 图标。

十八、多设备宽度下首页入口变化

HomePageonAreaChange 更新宽度:

.onAreaChange((_o, n) => {
  this.screenWidth = n.width as number
})

宽度大于 600 时热门实验从 4 个增到 6 个,网格变成两列或三列。需要验证窗口缩放时卡片数量变化不会让滚动位置突跳,入口顺序也保持一致。

private gridColumns(): string {
  if (this.screenWidth > 840) {
    return '1fr 1fr 1fr'
  }
  return '1fr 1fr'
}

phone、tablet、2in1 共享入口模型,只改变布局,不改变导航语义。

十九、深浅色与可访问性

项目启动链路锁定浅色模式,但导航令牌仍要集中维护。Tab 文本当前为 10vp,需要结合 UX 标准和真实设备检查可读性;普通手机文字通常应尽量保持 12vp 及以上。

同时验证:

  • 选中与未选中状态有足够对比;
  • 图标资源在浅色背景下清晰;
  • 长 Tab 文案不会挤压;
  • 2in1 键盘焦点可见;
  • 返回按钮拥有足够触控区域。

二十、导航清单的自动一致性检查

可以读取 main_pages.json,扫描 AppRoutes 中的路径:

interface RouteAuditItem {
  route: string
  declared: boolean
  sourceExists: boolean
}

构建检查至少验证:

  1. 常量路径已声明;
  2. 对应 .ets 文件存在;
  3. 首屏 pages/Index 位于清单;
  4. 不存在大小写不一致;
  5. 已删除页面没有遗留入口。

这类检查比运行时点击所有链接更早发现机械错误。

二十一、测试主 Tab 与路由契约

主 Tab 测试:

const tabCases = [
  { action: 'home', expected: MainTab.HOME },
  { action: 'lab', expected: MainTab.LAB },
  { action: 'learning', expected: MainTab.LEARNING },
  { action: 'mine', expected: MainTab.MINE }
]

实验路由测试:

  • stable_orbit 解析为稳定双体;
  • sandbox 解析为自由宇宙;
  • 热门卡片传真实实验 ID;
  • 空 ID 被拒绝;
  • 未知 ID 进入 notFound;
  • 返回后二级页面出栈,原 Tab 保持;
  • 快速双击只压入一次。

二十二、常见问题与修复顺序

现象 优先检查 修复方向
首页“关卡模式”跳错 Tab 裸数字索引 使用 MainTab
返回出现多个首页 是否 push 了 Index 二级页使用 back
相关实验进入默认场景 expId 是否缺失 来源相关入口禁止默认
页面标题与场景不符 是否信任 expName 由目录按 ID 解析
点击无响应 路径、清单与 Promise 错误 返回可见失败状态
双击进入两层页面 是否缺少导航锁 阻止重复压栈
Tab 状态丢失 Index 是否被重建 区分 Tab 切换和 Router
平板入口布局跳动 宽度阈值与卡片数量 验证 resize 行为

定位时先看入口类型,再看路径清单,然后看参数解析与返回栈。不要把所有问题都归因于 Router。

二十三、发布前验证清单

  • [ ] 四个主 Tab 使用业务枚举;
  • [ ] Tab 切换不创建新首页;
  • [ ] 二级页面统一使用路径常量;
  • [ ] 实验入口都携带稳定 expId
  • [ ] 接收页校验运行时参数;
  • [ ] 标题由实验目录解析;
  • [ ] 默认场景只用于明确入口;
  • [ ] 返回不会重复压入 Index;
  • [ ] 快速点击不产生重复页面;
  • [ ] 导航失败有可见反馈;
  • [ ] 页面路径与 main_pages.json 一致;
  • [ ] phone、tablet、2in1 导航均可达;
  • [ ] release 包完成启动、切 Tab、进详情、返回和退出冒烟。

总结

“天体运行模拟”的主导航已经形成清晰基础:Index 管理四个 Tab,HomePage 通过回调切换关卡域,通过 Router 打开实验二级页。真正需要补强的是契约,而不是推翻现有结构。

MainTab 替代裸数字,用 AppRoutes 集中路径,用稳定 ID 构造参数,用目录解析当前模型,再把返回和重复点击纳入测试,主导航就能从“能跳转”升级为“入口统一、返回可预测、参数可验证”。这套方法同样适用于知识页、收藏页和实验结果页。

本文唯一标记:CSDN-SERIES:ALL-163200848

AI 辅助声明:本文部分内容由 AI 辅助整理,源码事实、工程边界与验证结论均依据文中所列项目文件复核。本文没有执行新的构建、导航回归、真机、键鼠或发布包验收,因此相关状态均不表述为已验证。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐