引言

如果你开发过移动端应用,你一定处理过这样的场景:用户从商品列表进入详情页,从详情页进入评论页,从评论页进入用户主页,然后点"返回"需要一层一层退回去。更复杂的是,产品经理可能会说"支付成功后,直接跳回首页,中间那些页面全部清掉"。

这些需求本质上都是对"导航栈"(Navigation Stack)的操作——页面就像一叠卡片,每次跳转push一张新卡片,每次返回pop掉最上面一张。但在复杂的业务场景中,我们需要的不仅仅是简单的push/pop:我们需要pop到指定页面(popTo),移除中间的某张卡片(remove),把某张卡片移到栈顶(moveToTop),甚至一次性清空整叠卡片(clear)。

HarmonyOS NEXT 的 ArkUI 提供了 NavPathStack ——一个完整的导航栈管理 API。它不是简单的"能跳转能返回",而是提供了对导航栈的精细控制能力。本文将结合 Navigation 组件,通过构建一个完整的"多步骤选购向导",深入讲解 NavPathStack 的全部核心 API。

读完本文你将能够:

  • 理解 NavPathStack 的数据结构和工作原理
  • 掌握 pushPathByName、pop、popToName、removeByName、moveToTop、clear 六种栈操作
  • 使用 @Builder PageMap 模式构建多页面导航
  • 实现导航栈状态的可视化监控
  • 理解 Navigation 与 NavPathStack 的配合机制

为什么不是 router?

在 HarmonyOS NEXT 之前,ArkUI 的页面路由主要靠 router.pushUrl()router.back()。这套 API 能解决基本的页面跳转问题,但有两个痛点是 router 无法优雅解决的:

第一,router 无法获取路由栈的当前状态。 router.getState() 返回的是当前页面的路由信息,不是整个栈的快照。你无法知道栈里有几层、每层是什么页面。而在 NavPathStack 中,一行 getAllPathName() 就能返回栈中所有页面的名称列表,size() 返回栈深度——这是构建调试面板、面包屑导航等功能的基础。

第二,router 无法做栈内定点跳转。 router 可以 pushUrl(入栈)、back(出栈)、replaceUrl(替换当前页)、clear(清栈后push新页)。但它不能 popTo(回到栈中某一页并弹出它上面的所有页)、不能 remove(从栈中间移除某一页)、不能 moveToTop(把栈中某页提到栈顶)。这些操作在复杂导航场景中是刚需。

NavPathStack 的出现填补了这些缺口。它不是 router 的"升级版",而是对导航栈操作模型的"重新思考"——把导航栈当作一等公民,赋予开发者完全的运行时控制能力。

Navigation + NavPathStack 基础架构

使用 NavPathStack 需要和 Navigation 组件配合。Navigation 是导航框架容器,NavPathStack 是它的数据核心。基本架构如下:

import { NavPathStack } from '@kit.ArkUI';

@Entry
@Component
struct MyPage {
  // 1. 创建 NavPathStack 实例
  private navPathStack: NavPathStack = new NavPathStack();

  @Builder
  PageMap(name: string) {
    // 2. 根据页面名称返回对应的 @Builder 内容
    if (name === 'pageA') {
      this.PageA()
    } else if (name === 'pageB') {
      this.PageB()
    }
  }

  @Builder
  PageA() { /* 页面A的UI */ }
  @Builder
  PageB() { /* 页面B的UI */ }

  build() {
    // 3. Navigation 绑定 NavPathStack 和 PageMap
    Navigation(this.navPathStack) {
      // 首页内容(栈为空时显示)
    }
    .navDestination(this.PageMap)  // 注册路由映射
    .hideTitleBar(true)
    .mode(NavigationMode.Stack)    // 栈模式(默认分栏模式)
  }
}

三个关键步骤:

  1. 创建 NavPathStacknew NavPathStack() 创建空栈。注意它用 private 而非 @State——NavPathStack 内部管理自己的状态变化,不需要 @State 装饰器触发更新。

  2. 定义 PageMap:一个 @Builder 函数,接收 name: string 参数,返回对应页面的 UI。它相当于 Vue Router 的 <router-view> 或 React Router 的 route config——页面名称到 UI 的映射表。

  3. 绑定 NavigationNavigation(this.navPathStack) 将栈实例绑定到导航组件。.navDestination(this.PageMap) 注册路由映射。.mode(NavigationMode.Stack) 设置为栈模式——这是单页面逐层导航的标准模式(另一个模式 NavigationMode.Split 用于平板分栏布局)。

多步骤选购向导:Demo 设计

本文的 Demo 是一个"选购向导"——用户从选择品类开始,每一步进入下一个选择页面,最终在确认页汇总所有选择。

四个步骤页面

  1. CategoryPage(品类选择):智能手机、平板电脑、智能手表、笔记本电脑、无线耳机
  2. BrandPage(品牌选择):华为、荣耀、小米、OPPO、vivo
  3. ModelPage(型号选择):标准版、Pro 版、Pro+ 版、至尊版
  4. ConfirmPage(确认选择):汇总品类、品牌、型号

**首页(导航栈为空时显示)**包含两个区域:

  • 导航栈状态面板:实时显示栈深度、栈中所有页面名称,每个条目旁边有删除按钮
  • 栈操作按钮区:开始选购 / Pop返回 / 回到品类 / 清除全部 / 移动品类置顶

这个设计刻意"过度展示"了导航栈的信息——在生产应用中你不会把栈详情展示给用户,但作为学习 Demo,可视化栈状态能帮助理解每一步操作的效果。

核心实现一:页面映射与入栈

PageMap 路由映射

@Builder
PageMap(name: string) {
  if (name === 'category') {
    this.CategoryPage()
  } else if (name === 'brand') {
    this.BrandPage()
  } else if (name === 'model') {
    this.ModelPage()
  } else if (name === 'confirm') {
    this.ConfirmPage()
  }
}

四个页面名称分别映射到四个 @Builder 方法。注意 PageMap 接收的是 name: string,这是 ArkUI 框架的约定——当 Navigation 需要渲染某个页面时,它会调用 navDestination 注册的 Builder 并传入页面名称。

pushPathByName 入栈

当用户在品类页点击某个品类时,使用 pushPathByName 进入品牌页:

this.selectedCategory = cat;
this.navPathStack.pushPathByName('brand', null);
this.refreshStackInfo();

pushPathByName 接收两个参数:

  • name:目标页面在 PageMap 中注册的名称
  • param:传递给目标页面的参数(本例中不需要参数传递,传 null

如果需要传递参数,可以这样做:

// 入栈时带参数
this.navPathStack.pushPathByName('detail', { id: 123, title: '商品详情' });

// 目标页面中获取参数
let param = this.navPathStack.getParamByName('detail');
// param.result 即为 { id: 123, title: '商品详情' }

还有一个 pushPath 方法,它接收 NavPathInfo 对象而非名称字符串。pushPathByName 是更常用的简化版。

refreshStackInfo:实时追踪栈状态

每次栈操作后都调用 refreshStackInfo() 更新状态面板:

refreshStackInfo(): void {
  let names = this.navPathStack.getAllPathName();  // 获取所有页面名称
  let size = this.navPathStack.size();              // 获取栈深度
  this.stackInfo = '栈深度: ' + size.toString();
  let entries: StackEntry[] = [];
  for (let i = 0; i < names.length; i++) {
    entries.push(new StackEntry(i, names[i]));
  }
  this.stackEntries = entries;
}

这个方法的两个核心调用:

  • getAllPathName(): string[] — 返回栈中所有页面的名称列表(从栈底到栈顶)
  • size(): number — 返回栈深度
    在这里插入图片描述
    在这里插入图片描述

核心实现二:pop 与 popToName

pop:逐层返回

每个步骤页面的左上角都有"← 返回"按钮:

Text('← 返回')
  .fontSize(14)
  .fontColor('#1677FF')
  .onClick(() => {
    this.navPathStack.pop();
    this.refreshStackInfo();
  })

pop() 弹出栈顶页面(当前页面),回到上一页。这是最基础的操作,相当于浏览器的"后退"按钮。

在首页的栈操作区,也有一个 Pop 按钮,做了防御性检查:

Button('Pop 返回')
  .onClick(() => {
    if (this.navPathStack.size() > 0) {
      this.navPathStack.pop();
      this.refreshStackInfo();
    }
  })

size() > 0 确保栈不为空时才执行 pop——如果栈已经空了还 pop,可能导致异常。

popToName:定点跳回

假设用户在品类页 → 品牌页 → 型号页 → 确认页,此时用户想直接回到品类页重新选择,不必一层层退回去:

Button('回到品类')
  .onClick(() => {
    this.navPathStack.popToName('category');
    this.refreshStackInfo();
  })

popToName('category') 会弹出类别页上面的所有页面(品牌页、型号页、确认页),但保留品类页本身。也就是说,它回到品类页,品类页仍然在栈中。

如果品类页本身也不在栈中(比如首页就被清掉了),popToName 的行为是:如果指定名称的页面存在于栈中,就pop到它;如果不存在,则不执行任何操作(不会报错)。

popToName 的典型应用场景:

  • 支付成功后跳回商品详情页
  • 表单提交成功后跳回列表页
  • "返回首页"按钮——popToName('index') 清掉所有中间页

核心实现三:removeByName

在首页的状态面板中,每个栈条目的右侧有一个"×"按钮:

Text('—')
  .fontSize(11)
  .fontColor('#CCCCDD')
  .padding({ left: 8 })
  .onClick(() => {
    this.navPathStack.removeByName(entry.name);
    this.refreshStackInfo();
  })

removeByName(name) 从栈中任意位置移除指定名称的页面。它不是只移除栈顶——它可以移除栈中间的某一页。

例如,栈结构为 [category, brand, model, confirm],如果 removeByName('brand'),栈变为 [category, model, confirm]。被移除页面上面的所有页面会依次下移。

注意:如果有多个同名页面,removeByName 只会移除第一个匹配的(从栈底开始搜索的第一个)。在本文 Demo 中每个页面名称唯一,所以不会遇到这个问题。

removeByName 的典型应用场景:

  • 某些中间步骤在特定条件下可以跳过(如 VIP 用户跳过广告页)
  • 业务流程优化后动态精简历史页面

核心实现四:moveToTop

"移动品类置顶"按钮演示了 moveToTop

Button('移动品类置顶')
  .onClick(() => {
    this.navPathStack.moveToTop('category');
    this.refreshStackInfo();
  })

moveToTop(name) 将指定名称的页面移到栈顶,不改变栈中的页面集合——只是改变顺序。

例如,栈结构为 [category, brand, model],执行 moveToTop('category') 后栈变为 [brand, model, category]。注意:这是把 category 移到了栈顶,成为了当前显示的页面,相当于"切换到这个标签页"。

如果指定名称的页面已经是栈顶,则不执行任何操作。

moveToTop 的典型应用场景:

  • Tab 切换时保留之前的页面状态
  • 查看历史页面(查看完后不 pop,而是 moveToTop 切回去)

核心实现五:clear

Button('清除全部')
  .onClick(() => {
    this.navPathStack.clear();
    this.selectedCategory = '未选择';
    this.selectedBrand = '未选择';
    this.selectedModel = '未选择';
    this.refreshStackInfo();
  })

clear() 清空整个导航栈,回到首页。这里同时重置了所有选择状态,确保用户从头开始选购。

注意 clear() 只清空栈,不会自动重置你的业务数据。你需要像上面那样手动重置 @State 变量。

完整页面结构

Column(根容器)
├── Navigation(绑定 navPathStack)
│   ├── 首页内容(栈为空时显示)
│   │   ├── Header(深色标题栏:"选购向导" + NavPathStack 标签)
│   │   ├── Scroll
│   │   │   └── Column
│   │   │       ├── 导航栈状态面板
│   │   │       │   ├── 标题 + 栈深度数字
│   │   │       │   └── 栈条目列表(索引 + 名称 + 删除按钮)
│   │   │       ├── 栈操作区
│   │   │       │   ├── "开始选购向导"按钮
│   │   │       │   ├── "Pop返回" + "回到品类"
│   │   │       │   └── "清除全部" + "移动品类置顶"
│   │   │       └── API 参考文字
│   │   └── 底部留白
│   └── .navDestination(PageMap) 注册的四个步骤页面
│       ├── category → CategoryPage(品类选择)
│       ├── brand → BrandPage(品牌选择)
│       ├── model → ModelPage(型号选择)
│       └── confirm → ConfirmPage(确认汇总)
└── 根容器结束

每个步骤页面的内部结构一致:

Column(步骤页)
├── 顶栏(←返回 + 步骤指示 1/4)
├── 内容区(标题 + 选项列表)
│   └── ForEach 选项列表
│       └── Row(选项文字 + 右箭头)
│           .选中态:蓝色边框 + 蓝色浅背景
│           .onClick → pushPathByName 进入下一步
└── 背景色 #F2F3F5

交互流程

完整的选购流程如下:

首页(栈为空)
  ↓ 点击"开始选购向导"
品类页(栈: [category])
  ↓ 点击"智能手机"
品牌页(栈: [category, brand])
  ↓ 点击"华为"
型号页(栈: [category, brand, model])
  ↓ 点击"Pro 版"
确认页(栈: [category, brand, model, confirm])

此时用户可以做以下任一操作:

  • 逐层返回:按 4 次"← 返回"回到首页
  • 回到品类:点击首页操作区的"回到品类"按钮,从确认页直接跳回品类页
  • 删除品牌页:在首页状态面板中点击 brand 旁边的"×",栈中间少一层
  • 移动品类置顶:品类页从栈底移到栈顶,成为当前显示页

步骤页面实现细节

选中态视觉反馈

每个选项的选中态通过条件样式实现:

.backgroundColor(this.selectedCategory === cat ? '#1677FF12' : '#F8F9FA')
.border({
  width: this.selectedCategory === cat ? 1.5 : 0,
  color: '#1677FF'
})

选中时:蓝色浅背景(#1677FF12 = 蓝色 + 约7%不透明度)+ 1.5px 蓝色边框。未选中时:灰色背景 + 无边框。这个模式在四个步骤页面中完全一致,保持了视觉统一。

步骤进度显示

每个步骤页面的顶栏右侧显示"步骤 X/4":

Text('步骤 1/4')  // 品类页
Text('步骤 2/4')  // 品牌页
Text('步骤 3/4')  // 型号页
Text('步骤 4/4')  // 确认页

虽然这个数字是硬编码的,但它给用户提供了清晰的进度感——知道自己在选购流程中的位置,以及还剩多少步骤。

确认页汇总

确认页用了一个简洁的键值对列表展示用户的所有选择:

Column() {
  this.confirmRow('品类', this.selectedCategory)
  this.confirmRow('品牌', this.selectedBrand)
  this.confirmRow('型号', this.selectedModel)
}
.width('100%')
.padding(16)
.borderRadius(10)
.backgroundColor('#F8F9FA')

confirmRow 是一个 @Builder 方法,渲染 label + value 的行布局,底部有细线分隔。

NavPathStack 全部 API 速览

本文 Demo 演示了 NavPathStack 的 8 个 API:

API 签名 功能 示例
pushPathByName (name: string, param: object) => void 将命名页面压入栈顶 pushPathByName('brand', null)
pop () => void 弹出栈顶页面 pop()
popToName (name: string) => void 回退到指定页面(保留该页) popToName('category')
removeByName (name: string) => void 移除栈中指定名称的页面 removeByName('brand')
moveToTop (name: string) => void 将指定页面移到栈顶 moveToTop('category')
clear () => void 清空全部页面 clear()
size () => number 返回当前栈深度 size()
getAllPathName () => string[] 返回栈中所有页面名称数组 getAllPathName()

此外,NavPathStack 还有两个本文未演示但很实用的 API:

API 签名 功能
pushPath (info: NavPathInfo) => void 使用 NavPathInfo 对象压栈(比 pushPathByName 更灵活)
getParamByName (name: string) => object | undefined 获取指定名称页面的参数
replacePathByName (name: string, param: object) => void 替换当前页面(pop 后 push,旧页面不保留)

适用场景

NavPathStack 的精细栈控制能力适合以下场景:

  1. 多步骤表单/向导(本文 Demo):用户按步骤填写信息,可以前进、后退、跳回某步、取消全部。

  2. 电商下单流程:商品详情 → 下单确认 → 支付 → 支付成功。支付成功后需要 popToName 回到商品详情,或者 clear 清栈后进入订单列表。

  3. 登录注册流程:首页 → 登录 → 验证码 → 设置密码 → 注册成功。注册成功后清栈回首页并刷新登录状态。登录成功后不仅 pop 登录页,还要 pop 掉触发登录的那个页面(如"我的"页面)。

  4. 复杂表单的中断恢复:用户在填写一个多页表单时切换到其他页面(如查看帮助文档),然后通过 moveToTop 回到表单页面,表单状态保留。

  5. 面包屑导航:用 getAllPathName() 获取栈中所有页面名称,渲染为面包屑路径,用户可以点击任意一级面包屑跳回对应页面。

与前端框架的路由对比

如果你有 React Router 或 Vue Router 的使用经验,可以这样理解 NavPathStack:

概念 React Router Vue Router NavPathStack
跳转 navigate('/page') router.push('/page') pushPathByName('page', param)
返回 navigate(-1) router.back() pop()
返回指定页 navigate(-n)(需自己算) router.go(-n)(需自己算) popToName('page')(语义化)
栈状态 useNavigationState() router.getRoutes() getAllPathName() + size()
路由映射 <Route path="/a" element={<A/>}/> routes: [{ path: '/a', component: A }] @Builder PageMap(name) { if/else }

关键区别:React/Vue 的路由映射是声明式配置文件,NavPathStack 的 PageMap 是函数式映射(if/else 判断)。后者更灵活——你可以在映射时根据运行时状态动态决定渲染哪个 Builder,但缺点是不支持路径参数匹配(如 /user/:id)和嵌套路由。

对于多步骤向导这类"线性流程+灵活跳转"的场景,NavPathStack 的 API 比前端路由库更直观:你不需要管理路径字符串的拼接和解析,直接用页面名称做操作。

注意事项

1. NavPathStack 的生命周期

NavPathStack 的生命周期应该与 @Entry 组件一致。使用 private navPathStack: NavPathStack = new NavPathStack() 确保它在页面创建时初始化一次。不要放在 build() 方法中——那会导致每次渲染都创建新实例,丢失栈状态。

2. 页面名称的唯一性

pushPathByNamepopToNameremoveByNamemoveToTop 都依赖页面名称。如果你的应用中可能存在多个同名页面(如多个商品详情页都叫 'detail'),API 的行为是匹配栈底开始的第一个。为了避免歧义,建议为每个概念页使用唯一名称,或者使用 pushPath 配合自定义的 NavPathInfo 来携带唯一标识。

3. Navigation 的 mode 选择

NavigationMode.Stack 是标准的单页栈导航——每次只显示一个页面,有返回动画。NavigationMode.Split 是分栏模式——左侧显示主页面,右侧显示从页面,适合平板设备。对于手机端的多步骤向导,Stack 模式是唯一正确的选择。

4. hideTitleBar 的作用

.hideTitleBar(true)

Navigation 默认自带一个标题栏。大多数应用使用自定义标题栏,所以需要隐藏默认标题栏。如果不隐藏,Navigation 的默认标题栏会叠加在你的自定义 Header 上面,造成双标题栏。

5. 状态同步

NavPathStack 内部的状态变化(push、pop、remove 等)会自动触发 Navigation 的重新渲染。但你的业务状态(如 selectedCategoryselectedBrand)不会自动与栈状态同步——你需要在栈操作的回调中手动更新业务状态。本文 Demo 在每次栈操作后都调用 refreshStackInfo() 来同步状态面板,在 clear() 时重置所有选择变量。

总结

本文通过构建一个完整的"多步骤选购向导",深入讲解了 HarmonyOS NEXT ArkUI 中 NavPathStack 导航栈管理的核心 API:

  1. pushPathByName:将命名页面压入栈顶,配合 PageMap 路由映射实现声明式导航
  2. pop:弹出栈顶,逐层返回
  3. popToName:回退到指定页面,跨层跳转的利器
  4. removeByName:移除栈中任意位置的页面,动态精简历史页面
  5. moveToTop:将指定页面移到栈顶,保留页面状态的切页
  6. clear:清空全部,一键回到起点
  7. size + getAllPathName:栈状态查询,构建栈可视化和面包屑的基础

NavPathStack 的设计哲学是"把导航栈当作一等公民"——开发者不再通过路径字符串间接操作导航,而是直接操作栈数据结构。push、pop、remove、moveToTop 这些 API 的名字本身就是栈操作的标准术语,学习曲线低,语义清晰。

对于复杂移动端应用的导航需求,NavPathStack 提供的精细控制能力远非传统的 push/replace/back 三件套可比。当你需要在支付成功后清掉中间页、在表单提交后回到列表、在注册完成后重置导航状态——这些场景下,NavPathStack 就是你的利器。

在某种意义上,导航栈管理是应用架构的"骨架"——它定义了用户在页面之间的流动性。骨架搭好了,肌肉(业务功能)才有附着点。NavPathStack 让这个骨架变得更灵活、更可控。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐