[鸿蒙从零到一] 页面路由与 Navigation 实战

简介: 本文详解HarmonyOS页面路由实战,对比`router`轻量跳转与`Navigation`栈式导航适用场景;涵盖登录拦截、参数传递、返回结果处理、底部Tab独立栈、横竖屏恢复等核心问题;倡导路由集中管理、参数最小化、页面解耦与工程化目录结构,助开发者构建稳定可扩展的鸿蒙应用骨架。

HarmonyOS 页面路由与 Navigation 实战

在 HarmonyOS 应用里,页面跳转不只是从 A 页打开 B 页这么简单。真正落到项目里,还会遇到登录拦截、参数传递、返回结果、底部导航、多级详情页、异常返回、横竖屏恢复等问题。把路由设计清楚,页面结构会更稳,后续做模块化和状态管理也会轻很多。

本文围绕 Stage 模型与 ArkUI 的实际开发场景,讲清楚两类常用页面组织方式:轻量页面跳转与 Navigation 栈式导航。前者适合简单入口,后者更适合中大型应用的主流程。

一、先分清页面跳转和应用拉起

鸿蒙应用端经常会同时接触两种“跳转”:

场景 常用能力 典型用途
应用内页面切换 routerNavigation 首页到详情页、设置页、表单页
Ability 之间拉起 Want / startAbility 从通知进入指定 Ability、跨模块入口、外部应用拉起

日常页面开发里,优先关注应用内页面切换。Ability 拉起属于系统调度层面的入口管理,适合放在外部链接、通知、服务入口这些位置。不要把所有页面都拆成 Ability,否则任务栈、生命周期和数据传递都会变复杂。

一个实用判断:

  • 同一个业务闭环里的页面,用 Navigation 组织。
  • 临时简单页面,可以用 router 快速打开。
  • 独立任务入口、外部可拉起能力,再考虑 Ability。

二、轻量路由:router 适合简单页面

router 是 ArkUI 提供的页面级跳转能力,使用成本低,适合新手理解页面切换,也适合一些简单应用。

假设有一个商品列表页,需要打开商品详情页,可以这样写:

import router from '@ohos.router'

Button('查看详情')
  .onClick(() => {
   
    router.pushUrl({
   
      url: 'pages/ProductDetail',
      params: {
   
        productId: 'p_1001',
        from: 'home'
      }
    })
  })

详情页读取参数:

import router from '@ohos.router'

interface ProductParams {
   
  productId?: string
  from?: string
}

@Entry
@Component
struct ProductDetailPage {
   
  @State productId: string = ''

  aboutToAppear(): void {
   
    const params = router.getParams() as ProductParams
    this.productId = params.productId ?? ''
  }

  build() {
   
    Column() {
   
      Text(`商品 ID:${
     this.productId}`)
      Button('返回')
        .onClick(() => router.back())
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

router.pushUrl 会把新页面压入页面栈,router.back 回到上一个页面。这个模型直观,但也有明显边界:当页面层级多、需要统一拦截、需要在不同 Tab 内维护独立栈时,router 会开始吃力。

三、Navigation 更适合主流程

Navigation 是 ArkUI 面向复杂页面结构提供的导航容器。它不是简单的 API 调用,而是把“页面栈”变成组件状态的一部分。这样做的好处是:

  • 页面结构更符合声明式 UI 思路。
  • 可以集中管理栈内页面、标题栏、返回按钮和页面参数。
  • 适合和状态管理、模块化路由表结合。
  • 在折叠屏、平板等多窗口场景下扩展空间更大。

一个最小的 Navigation 结构如下:

@Entry
@Component
struct MainPage {
   
  private pathStack: NavPathStack = new NavPathStack()

  build() {
   
    Navigation(this.pathStack) {
   
      HomeContent({
    pathStack: this.pathStack })
    }
    .title('首页')
    .navDestination(this.pageMap)
  }

  @Builder
  pageMap(name: string, param: object) {
   
    if (name === 'ProductDetail') {
   
      ProductDetail({
    productId: (param as Record<string, string>)['productId'] })
    } else if (name === 'Settings') {
   
      SettingsPage()
    }
  }
}

首页组件通过 NavPathStack 打开目标页:

@Component
struct HomeContent {
   
  pathStack: NavPathStack

  build() {
   
    Column({
    space: 12 }) {
   
      Button('商品详情')
        .onClick(() => {
   
          this.pathStack.pushPath({
   
            name: 'ProductDetail',
            param: {
    productId: 'p_1001' }
          })
        })

      Button('设置')
        .onClick(() => {
   
          this.pathStack.pushPath({
    name: 'Settings' })
        })
    }
    .padding(16)
  }
}

这里的核心是:页面名称和参数进入 NavPathStack,真正渲染哪个页面由 .navDestination 统一决定。业务页面不需要知道目标页面的创建细节,只要知道路由名称和参数即可。

四、用路由表降低耦合

项目变大后,不建议在 pageMap 里堆很多 if。可以把路由名称、参数类型和构建逻辑收束到一个位置。

先定义路由常量:

export const RouteName = {
   
  PRODUCT_DETAIL: 'ProductDetail',
  ORDER_CONFIRM: 'OrderConfirm',
  SETTINGS: 'Settings'
} as const

export interface ProductDetailParam {
   
  productId: string
}

export interface OrderConfirmParam {
   
  skuId: string
  count: number
}

再封装跳转方法:

export class AppNavigator {
   
  constructor(private readonly pathStack: NavPathStack) {
   }

  openProductDetail(param: ProductDetailParam): void {
   
    this.pathStack.pushPath({
   
      name: RouteName.PRODUCT_DETAIL,
      param
    })
  }

  openOrderConfirm(param: OrderConfirmParam): void {
   
    this.pathStack.pushPath({
   
      name: RouteName.ORDER_CONFIRM,
      param
    })
  }

  back(): void {
   
    this.pathStack.pop()
  }
}

业务代码就会变得更干净:

Button('立即购买')
  .onClick(() => {
   
    this.navigator.openOrderConfirm({
   
      skuId: this.skuId,
      count: this.buyCount
    })
  })

这样做的价值不在于少写几行代码,而是把路由名称、参数结构和跳转行为固定下来。后面新增登录拦截、埋点、异常兜底时,只需要改导航层。

五、参数传递要少而稳定

页面参数最好只传“能标识资源的最小数据”,例如 productIdorderIdtabKey。不要把整份接口返回对象直接塞进路由参数。

推荐写法:

this.pathStack.pushPath({
   
  name: RouteName.PRODUCT_DETAIL,
  param: {
    productId: item.id }
})

详情页根据 ID 拉取数据或从统一 Store 读取缓存:

@Component
struct ProductDetail {
   
  productId: string
  @State productName: string = ''
  @State loading: boolean = true

  aboutToAppear(): void {
   
    this.loadProduct()
  }

  async loadProduct(): Promise<void> {
   
    this.loading = true
    try {
   
      const product = await ProductRepository.getById(this.productId)
      this.productName = product.name
    } finally {
   
      this.loading = false
    }
  }

  build() {
   
    Column() {
   
      if (this.loading) {
   
        LoadingProgress()
      } else {
   
        Text(this.productName).fontSize(20).fontWeight(FontWeight.Medium)
      }
    }
    .padding(16)
  }
}

这种方式更抗变化。列表页刷新、详情页恢复、应用进程重建时,都不依赖一份可能已经过期的大对象。

六、返回结果不要强行共享组件状态

常见场景:编辑地址页保存后,返回地址列表并刷新。初学者容易把列表页的状态对象传给编辑页修改,这会让页面耦合得很紧。

更稳的方式是用统一数据源或事件通知。保存成功后返回,列表页在重新出现时刷新:

@Component
struct AddressListPage {
   
  @State addresses: Address[] = []

  aboutToAppear(): void {
   
    this.refresh()
  }

  async refresh(): Promise<void> {
   
    this.addresses = await AddressRepository.list()
  }

  build() {
   
    List() {
   
      ForEach(this.addresses, (item: Address) => {
   
        ListItem() {
   
          Text(item.detail)
        }
      })
    }
  }
}

编辑页只负责保存和返回:

Button('保存')
  .onClick(async () => {
   
    await AddressRepository.save(this.formData)
    this.pathStack.pop()
  })

如果业务需要明确结果,可以在共享 Store 中维护一个版本号或更新时间戳,列表页检测到变化再刷新。重点是不要让两个页面互相持有对方的内部状态。

七、登录拦截放在导航入口

登录拦截是路由系统最能体现价值的地方。比如订单确认页必须登录,商品详情页可以游客访问。不要在每个按钮里都写一遍登录判断,而是把规则放进导航层。

const authRequiredRoutes: string[] = [
  RouteName.ORDER_CONFIRM,
  'UserProfile',
  'CouponCenter'
]

export class GuardedNavigator {
   
  constructor(
    private readonly pathStack: NavPathStack,
    private readonly session: UserSession
  ) {
   }

  push(name: string, param?: object): void {
   
    if (authRequiredRoutes.includes(name) && !this.session.isLogin()) {
   
      this.pathStack.pushPath({
   
        name: 'Login',
        param: {
    redirect: name, redirectParam: param }
      })
      return
    }

    this.pathStack.pushPath({
    name, param })
  }
}

登录成功后读取 redirect,再进入原目标页。这样用户体验更完整,也避免了业务页到处散落鉴权逻辑。

八、底部 Tab 与独立页面栈

很多应用首页会有底部 Tab:主页、分类、消息、我的。一个细节是,每个 Tab 往往需要自己的页面栈。用户在分类 Tab 进入二级分类,再切到主页,回来时应该还停留在二级分类。

可以为不同 Tab 准备独立的 NavPathStack

@Entry
@Component
struct TabRootPage {
   
  @State currentIndex: number = 0
  private homeStack: NavPathStack = new NavPathStack()
  private categoryStack: NavPathStack = new NavPathStack()
  private messageStack: NavPathStack = new NavPathStack()
  private profileStack: NavPathStack = new NavPathStack()

  build() {
   
    Tabs({
    index: this.currentIndex }) {
   
      TabContent() {
   
        Navigation(this.homeStack) {
    HomePage({
    pathStack: this.homeStack }) }
          .navDestination(this.homeDestinations)
      }
      .tabBar('首页')

      TabContent() {
   
        Navigation(this.categoryStack) {
    CategoryPage({
    pathStack: this.categoryStack }) }
          .navDestination(this.categoryDestinations)
      }
      .tabBar('分类')

      TabContent() {
   
        Navigation(this.messageStack) {
    MessagePage({
    pathStack: this.messageStack }) }
          .navDestination(this.messageDestinations)
      }
      .tabBar('消息')

      TabContent() {
   
        Navigation(this.profileStack) {
    ProfilePage({
    pathStack: this.profileStack }) }
          .navDestination(this.profileDestinations)
      }
      .tabBar('我的')
    }
  }
}

如果所有 Tab 共用一个栈,切换体验会非常混乱。独立栈虽然多写一点结构,但更符合用户对多入口应用的预期。

九、返回行为要有明确策略

页面栈管理里,返回行为容易被低估。推荐至少处理三类返回:

  • 普通返回:当前栈有页面时 pop
  • 根页面返回:首页根节点可以提示再次返回退出,或交给系统默认行为。
  • 任务完成返回:提交订单成功后清理中间页,回到订单详情或首页。

例如支付成功后,不希望用户返回到确认订单页,可以重置栈:

this.pathStack.clear()
this.pathStack.pushPath({
   
  name: 'OrderDetail',
  param: {
    orderId: result.orderId }
})

对登录、下单、发布内容这类流程,栈清理比简单 pop 更重要。否则用户可能通过返回键回到已经失效的中间状态。

十、工程里的推荐目录结构

一个可维护的路由目录可以这样组织:

entry/src/main/ets/
├── common/
│   └── router/
│       ├── RouteName.ets
│       ├── RouteParam.ets
│       ├── AppNavigator.ets
│       └── RouteGuard.ets
├── pages/
│   ├── home/
│   ├── product/
│   ├── order/
│   └── profile/
└── entryability/

路由层负责“去哪里”和“能不能去”,页面层负责“怎么展示”和“怎么交互”。只要边界清晰,后续做多模块、沉浸式标题栏、页面埋点都会更容易。

十一、常见问题与处理建议

问题 原因 建议
返回后页面数据没刷新 页面没有重新拉取数据 在页面出现时按需刷新,或监听 Store 版本变化
参数取不到 路由名称写错、参数结构不一致 用常量和接口约束参数,不手写散落字符串
Tab 切换后栈丢失 多个 Tab 共用同一个栈 每个 Tab 维护独立 NavPathStack
登录后无法回到原页面 没保存目标路由和参数 登录页接收 redirectredirectParam
返回到失效流程页 提交成功后只做了普通返回 对完成态流程清理或重建页面栈

十二、落地建议

如果你正在做一个真实鸿蒙项目,可以按这个顺序演进:

  • 原型和小工具页面用 router 快速完成跳转。
  • 主业务页面统一迁到 Navigation
  • 把路由名称、参数类型、页面构建和跳转方法集中管理。
  • 对登录、埋点、异常路由做统一拦截。
  • 对首页 Tab 使用独立页面栈。
  • 对订单、支付、发布等流程设计明确的栈清理策略。

页面路由看起来是基础能力,但它决定了应用骨架是否稳定。早期把路由边界定清楚,后面做状态管理、数据持久化和跨端适配时,会少很多隐性耦合。

相关文章
|
4天前
|
缓存 人工智能 安全
GPT-5.6 Terra与GPT-5.5性能实测:成本减半后的跑分对比与快速迁移指南
GPT-5.6 Terra 的定价为每百万 token 输入 2.50/输出 15。GPT-5.5 则是 5/ 30。Terra 的每一项费率,包括 $0.25/M 的缓存读取,都恰好是 GPT-5.5 的一半,因此在任何工作负载组合下,Terra 都固定 便宜 2.0x。以每天 10 万次请求、3K token 提示词计算,大约是 Terra 每天 2,000,GPT−5.5每天 4,000,即每月约 60,000对 120,000。问题在于:OpenAI 没有发布任何针对 Terra 的编码基准。那个著名的 91.9% Terminal-Bench 数字是 Sol 在 Ul
|
4天前
|
SQL 人工智能 自然语言处理
大模型内容安全实时防护:恶意Prompt注入拦截、越权阻断与熔断机制方案.166
本文系统阐述大模型输入安全防护体系,涵盖提示词注入、恶意Prompt拦截、越权阻断与输入熔断四大核心风险及应对方案。提出四层防护架构(预处理、检测、鉴权、熔断),结合规则引擎、语义识别与RBAC权限控制,实现全链路实时防护,保障业务合规、数据安全与服务稳定。
237 1
|
28天前
|
Linux 程序员 数据格式
【2026最新】Notepad++下载、安装和使用一篇搞定(附中文版安装包)
Notepad++ 是一款免费开源、轻量高效的 Windows 文本编辑器,支持 C/Python/HTML 等 80+ 语言语法高亮、代码折叠、正则替换、编码转换及插件扩展,专为程序员与文本处理用户打造,完美替代系统记事本。(239字)
|
4天前
|
存储 人工智能 JSON
Qwen 本地部署搭配 ComfyUI 生成 AI 漫剧完整实操指南(小白零基础可落地,零成本无限生成+角色一致性天花板)
2026全网最优本地漫剧流水线:零成本、离线运行、角色统一、低配(8G显卡)可跑。融合Qwen本地大模型+ComfyUI双引擎,实现剧本生成→分镜绘图→动态成片全自动,隐私安全、无审核限流,新手30分钟上手,日更无忧。(239字)
|
12天前
|
人工智能 编解码 物联网
2026 最新Stable Diffusion 本地部署教程 下载安装使用详细图解(含官网安装包)
Stable Diffusion(SD)是2022年发布的开源文生图模型,由Stability AI等联合开发。支持文生图、图生图、局部重绘等,依托VAE降低算力需求,可在消费级显卡运行。本文提供秋葉aaaki制作的Windows整合包(含图形界面与插件),开箱即用,零配置启动。
|
4天前
|
人工智能 缓存 JSON
刚刚 GPT-5.6 发布,吊打 Claude 5 和 Grok 4.5?一手实测来啦!
GPT-5.6 刚发布,跑分号称超越 Fable 5,真的假的?附一手实测,让 3 个最强 AI 编程模型 GPT-5.6 Sol、Claude Fable 5、Grok 4.5 在 Cursor 里同时一把梭开发网页游戏,看看最新模型到底谁更能打。
290 0
|
22天前
|
存储 人工智能 监控
QoderWork完全指南:从入门到精通,把“AI实习生”变成你的全能工作搭档
阿里云2026年推出的桌面端AI工作助手QoderWork,不止聊天,更可动手干活:本地运行、安全可控,支持文件整理、数据分析、PPT生成、网页开发等;内置专家套件、多Agent协作与自定义Skills,让AI真正成为你身边的“AI实习生”。
|
13天前
|
人工智能 缓存 安全
Claude Code 封号真实原因曝光,这次彻底不装了,直接针对国内开发者的账号下手?
Claude Code 封号潮背后:逆向扒出客户端隐写区域标记,Anthropic 政策收紧叠加 DeepSeek 7 月涨价,国产替代更紧迫。