鸿蒙Router与Navigation组件导航示例详解
适用版本:HarmonyOS NEXT / API 12+
语言:ArkTS
一、两种导航方案概览
鸿蒙提供了两套页面导航方案,适用于不同场景:
| 对比维度 | Router(路由) | Navigation(组件导航) |
|---|---|---|
| 出现版本 | 早期版本 | API 9+,API 12 成熟 |
| 导航粒度 | 页面级(整页跳转) | 组件级(灵活嵌套) |
| 栈管理 | 系统统一管理 | 开发者手动管理 NavPathStack |
| 传参方式 | 路由参数 params | NavPathStack 携带强类型参数 |
| 适配多端 | 手机较好,平板一般 | 原生支持响应式(手机/平板/折叠屏) |
| 生命周期 | onPageShow / onPageHide | onReady / onShown / onHidden |
| 动画 | 系统默认动画 | 完全自定义转场动画 |
| 推荐程度 | 旧项目维护 | 新项目首选 |
二、Router:传统页面路由
2.1 基本配置
Router 基于 文件路径 做路由,页面必须在 main_pages.json 中注册。
main_pages.json
{
"src": [
"pages/Index",
"pages/DetailPage",
"pages/ProfilePage",
"pages/LoginPage"
]
}每个页面的 .ets 文件必须加 @Entry 装饰器:
// pages/DetailPage.ets
@Entry
@Component
struct DetailPage {
build() {
Column() {
Text('详情页')
}
}
}
2.2 页面跳转
import { router } from '@ohos.router';
// ① 普通跳转(可返回)
router.pushUrl({
url: 'pages/DetailPage',
params: {
id: '123',
title: '商品详情',
}
});
// ② 替换当前页(不可返回)
router.replaceUrl({
url: 'pages/LoginPage',
});
// ③ 跳转并清空历史栈
router.pushUrl(
{ url: 'pages/Index' },
router.RouterMode.Single // 单实例模式:若栈中已有该页则移到顶部
);
2.3 接收参数
// pages/DetailPage.ets
import { router } from '@ohos.router';
interface DetailParams {
id: string;
title: string;
}
@Entry
@Component
struct DetailPage {
private params: DetailParams = router.getParams() as DetailParams;
build() {
Column() {
Text(`ID: ${this.params.id}`)
Text(`标题: ${this.params.title}`)
}
}
}
2.4 返回与返回传值
// 简单返回
router.back();
// 返回到指定页面
router.back({ url: 'pages/Index' });
// 返回并带数据(通过 params,接收方在 onPageShow 中获取)
router.back({
url: 'pages/OrderPage',
params: { refreshList: true }
});
// OrderPage.ets 在 onPageShow 中接收返回数据
@Entry
@Component
struct OrderPage {
onPageShow() {
const params = router.getParams() as Record<string, boolean>;
if (params?.refreshList) {
this.loadOrders();
}
}
loadOrders() { /* ... */ }
build() { /* ... */ }
}
2.5 Router 完整案例:商品列表 → 详情
列表页 ProductListPage.ets
import { router } from '@ohos.router';
interface Product {
id: string;
name: string;
price: number;
}
@Entry
@Component
struct ProductListPage {
private products: Product[] = [
{ id: '1', name: '苹果', price: 5 },
{ id: '2', name: '橙子', price: 8 },
{ id: '3', name: '葡萄', price: 15 },
];
build() {
Column() {
Text('商品列表').fontSize(20).fontWeight(FontWeight.Bold).margin(16)
List({ space: 12 }) {
ForEach(this.products, (item: Product) => {
ListItem() {
Row() {
Text(item.name).fontSize(16).layoutWeight(1)
Text(`¥${item.price}`).fontSize(16).fontColor('#FF5722')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
}
.onClick(() => {
router.pushUrl({
url: 'pages/ProductDetailPage',
params: { id: item.id, name: item.name, price: item.price }
});
})
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
详情页 ProductDetailPage.ets
import { router } from '@ohos.router';
interface ProductParams {
id: string;
name: string;
price: number;
}
@Entry
@Component
struct ProductDetailPage {
private product: ProductParams = router.getParams() as ProductParams;
@State inCart: boolean = false;
build() {
Column() {
// 顶部导航栏
Row() {
Image($r('app.media.ic_back'))
.width(24).height(24)
.onClick(() => router.back())
Text('商品详情').fontSize(18).fontWeight(FontWeight.Medium).margin({ left: 16 })
}
.width('100%').height(56).padding({ left: 16 })
// 内容
Column({ space: 12 }) {
Text(this.product.name).fontSize(24).fontWeight(FontWeight.Bold)
Text(`¥${this.product.price}`).fontSize(20).fontColor('#FF5722')
Text('商品描述:这是一款优质的新鲜水果,产地直供。').fontSize(14).fontColor('#666')
}
.width('100%').padding(16).alignItems(HorizontalAlign.Start)
// 加入购物车
Button(this.inCart ? '已加入购物车' : '加入购物车')
.width('90%')
.backgroundColor(this.inCart ? '#999' : '#FF5722')
.onClick(() => {
this.inCart = true;
// 返回时通知列表页刷新购物车数量
router.back({
url: 'pages/ProductListPage',
params: { cartUpdated: true }
});
})
}
.width('100%').height('100%')
}
}
2.6 Router 的局限性
- 必须注册页面路径,动态路由不方便
- 参数是 Object 类型,没有 TypeScript 类型推断
- 平板适配差,无法天然实现左右分栏
- 无法细粒度控制转场动画
- 返回传值麻烦,需要在
onPageShow中手动获取
三、Navigation:组件化导航(推荐)
3.1 核心概念
Navigation 由三个核心部分组成:
Navigation(导航容器)
│
├── NavPathStack(路由栈,由开发者创建和管理)
│
└── NavDestination(导航目标页,替代 @Entry 页面)
└── 通过 navDestination Builder 动态创建关键特点:
NavPathStack是强类型的路由栈,可携带任意类型参数NavDestination是组件,不需要在main_pages.json注册- 支持响应式布局(
NavigationMode.Auto自动切换单/双栏)
3.2 最简单的 Navigation 示例
// Index.ets(入口页)
@Entry
@ComponentV2
struct Index {
// ① 创建并管理路由栈
private stack: NavPathStack = new NavPathStack();
build() {
// ② Navigation 作为根容器,绑定路由栈
Navigation(this.stack) {
// 主页内容
Column() {
Text('首页').fontSize(24)
Button('跳转详情页')
.onClick(() => {
// ③ 通过路由栈跳转
this.stack.pushPathByName('DetailPage', { id: '123' });
})
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
// ④ 注册所有目标页面
.navDestination(pageMap)
.mode(NavigationMode.Stack)
.hideTitleBar(true)
}
}
// ⑤ 路由映射表(Builder 函数)
@Builder
function pageMap(name: string, param: object) {
if (name === 'DetailPage') {
DetailPage({ param: param as DetailParam })
}
}
// DetailPage.ets(目标页)
interface DetailParam {
id: string;
}
@ComponentV2
struct DetailPage {
@Param param: DetailParam = { id: '' };
// 通过 @Consumer 获取路由栈(可选,也可通过 NavDestinationContext)
@Consumer() stack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Column() {
Text(`详情 ID: ${this.param.id}`)
Button('返回')
.onClick(() => this.stack.pop())
}
.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
.title('详情页')
}
}
3.3 NavPathStack 常用 API
const stack = new NavPathStack();
// === 跳转 ===
stack.pushPathByName('PageName', params); // 压栈跳转
stack.pushPathByName('PageName', params, false); // 跳转但不加动画
stack.replacePath({ name: 'LoginPage' }); // 替换当前页(不可返回)
stack.pushPath({ name: 'PageName', param: {} }); // 使用 NavPathInfo 跳转
// === 返回 ===
stack.pop(); // 返回上一页
stack.pop({ result: 'ok' }); // 返回并携带结果
stack.popToName('HomePage'); // 返回到指定页面
stack.popToIndex(0); // 返回到栈底
stack.clear(); // 清空路由栈
// === 查询 ===
stack.size(); // 栈的深度
stack.getAllPathName(); // 获取所有页面名称列表
stack.getParamByName('PageName'); // 获取某页面的参数
3.4 接收返回值
Navigation 支持优雅的返回值回调,不需要像 Router 那样在 onPageShow 中判断:
// 跳转时注册回调
stack.pushPathByName('EditPage', { userId: '123' }, (popInfo) => {
// 当 EditPage 调用 stack.pop(result) 时,这里收到返回值
const result = popInfo.result as EditResult;
if (result.saved) {
this.loadUserInfo(); // 刷新数据
}
});
// EditPage 返回时携带结果
@ComponentV2
struct EditPage {
@Consumer() stack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('保存并返回')
.onClick(() => {
// pop 时传入结果
this.stack.pop({ saved: true, nickname: '新昵称' });
})
}
}
}
3.5 完整案例:电商 App 导航架构
这是一个完整的三层导航案例:首页 → 商品列表 → 商品详情。
第一步:定义路由常量和参数类型
// constants/RouteNames.ets
export const RouteNames = {
PRODUCT_LIST: 'ProductListPage',
PRODUCT_DETAIL: 'ProductDetailPage',
ORDER_CONFIRM: 'OrderConfirmPage',
LOGIN: 'LoginPage',
};
// 各页面参数类型
export interface ProductListParam {
categoryId: string;
categoryName: string;
}
export interface ProductDetailParam {
productId: string;
productName: string;
price: number;
}
export interface OrderConfirmParam {
productId: string;
quantity: number;
}
第二步:创建路由映射(集中管理)
// router/PageMap.ets
import { RouteNames, ProductListParam, ProductDetailParam, OrderConfirmParam } from '../constants/RouteNames';
import { ProductListPage } from '../pages/ProductListPage';
import { ProductDetailPage } from '../pages/ProductDetailPage';
import { OrderConfirmPage } from '../pages/OrderConfirmPage';
import { LoginPage } from '../pages/LoginPage';
@Builder
export function AppPageMap(name: string, param: object) {
if (name === RouteNames.PRODUCT_LIST) {
ProductListPage({ param: param as ProductListParam })
} else if (name === RouteNames.PRODUCT_DETAIL) {
ProductDetailPage({ param: param as ProductDetailParam })
} else if (name === RouteNames.ORDER_CONFIRM) {
OrderConfirmPage({ param: param as OrderConfirmParam })
} else if (name === RouteNames.LOGIN) {
LoginPage()
}
}
第三步:入口页(首页)
// pages/Index.ets
import { AppPageMap } from '../router/PageMap';
import { RouteNames, ProductListParam } from '../constants/RouteNames';
@Entry
@ComponentV2
struct Index {
@Provider() stack: NavPathStack = new NavPathStack();
private categories = [
{ id: 'fruit', name: '水果生鲜' },
{ id: 'snack', name: '零食饮料' },
{ id: 'daily', name: '日用百货' },
];
build() {
Navigation(this.stack) {
Scroll() {
Column({ space: 16 }) {
Text('首页').fontSize(24).fontWeight(FontWeight.Bold)
// 分类入口
ForEach(this.categories, (cat: { id: string; name: string }) => {
Row() {
Text(cat.name).fontSize(16).layoutWeight(1)
Image($r('app.media.ic_arrow_right')).width(16).height(16)
}
.width('100%').padding(16)
.backgroundColor(Color.White).borderRadius(8)
.onClick(() => {
const param: ProductListParam = {
categoryId: cat.id,
categoryName: cat.name,
};
this.stack.pushPathByName(RouteNames.PRODUCT_LIST, param);
})
})
}
.padding(16)
}
}
.navDestination(AppPageMap)
.mode(NavigationMode.Stack)
.hideTitleBar(true)
.width('100%').height('100%')
}
}
第四步:商品列表页
// pages/ProductListPage.ets
import { RouteNames, ProductListParam, ProductDetailParam } from '../constants/RouteNames';
interface Product {
id: string;
name: string;
price: number;
}
@ComponentV2
export struct ProductListPage {
@Param param: ProductListParam = { categoryId: '', categoryName: '' };
@Consumer() stack: NavPathStack = new NavPathStack();
// 模拟数据
private getProducts(): Product[] {
return [
{ id: 'p1', name: `${this.param.categoryName}-商品A`, price: 19.9 },
{ id: 'p2', name: `${this.param.categoryName}-商品B`, price: 35.5 },
{ id: 'p3', name: `${this.param.categoryName}-商品C`, price: 8.8 },
];
}
build() {
NavDestination() {
List({ space: 12 }) {
ForEach(this.getProducts(), (product: Product) => {
ListItem() {
Row() {
Column({ space: 4 }) {
Text(product.name).fontSize(16)
Text(`¥${product.price.toFixed(2)}`).fontSize(14).fontColor('#FF5722')
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Button('查看详情').fontSize(12)
.onClick(() => {
const param: ProductDetailParam = {
productId: product.id,
productName: product.name,
price: product.price,
};
// 跳转详情并接收返回值
this.stack.pushPathByName(RouteNames.PRODUCT_DETAIL, param, (popInfo) => {
const result = popInfo.result as { addedToCart: boolean };
if (result?.addedToCart) {
console.info('用户已加入购物车,刷新角标');
}
});
})
}
.width('100%').padding(16)
.backgroundColor(Color.White).borderRadius(8)
}
})
}
.padding(16)
}
.title(this.param.categoryName)
.backgroundColor('#F5F5F5')
}
}
第五步:商品详情页
// pages/ProductDetailPage.ets
import { RouteNames, ProductDetailParam, OrderConfirmParam } from '../constants/RouteNames';
@ComponentV2
export struct ProductDetailPage {
@Param param: ProductDetailParam = { productId: '', productName: '', price: 0 };
@Consumer() stack: NavPathStack = new NavPathStack();
@Local quantity: number = 1;
build() {
NavDestination() {
Column({ space: 0 }) {
// 商品信息区
Column({ space: 12 }) {
Text(this.param.productName)
.fontSize(22).fontWeight(FontWeight.Bold)
Text(`¥${this.param.price.toFixed(2)}`)
.fontSize(20).fontColor('#FF5722')
Text('产地直供,新鲜直达,品质保证。')
.fontSize(14).fontColor('#888').lineHeight(22)
}
.width('100%').padding(16).alignItems(HorizontalAlign.Start)
.backgroundColor(Color.White)
// 数量选择
Row({ space: 16 }) {
Text('数量').fontSize(15)
Row({ space: 12 }) {
Button('-').width(32).height(32)
.onClick(() => { if (this.quantity > 1) this.quantity--; })
Text(`${this.quantity}`).fontSize(16).width(40).textAlign(TextAlign.Center)
Button('+').width(32).height(32)
.onClick(() => { this.quantity++; })
}
}
.width('100%').padding(16).justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(Color.White).margin({ top: 8 })
Blank()
// 底部按钮区
Row({ space: 12 }) {
Button('加入购物车')
.layoutWeight(1).backgroundColor('#FF9800')
.onClick(() => {
// 返回并携带结果给列表页
this.stack.pop({ addedToCart: true });
})
Button('立即购买')
.layoutWeight(1).backgroundColor('#FF5722')
.onClick(() => {
const param: OrderConfirmParam = {
productId: this.param.productId,
quantity: this.quantity,
};
this.stack.pushPathByName(RouteNames.ORDER_CONFIRM, param);
})
}
.width('100%').padding(16)
.backgroundColor(Color.White)
.border({ width: { top: 1 }, color: '#F0F0F0' })
}
.width('100%').height('100%').backgroundColor('#F5F5F5')
}
.title(this.param.productName)
}
}
3.6 NavigationMode 响应式布局(平板适配)
Navigation 最大的优势之一是原生支持响应式双栏布局,一套代码适配手机和平板。
@Entry
@ComponentV2
struct Index {
@Provider() stack: NavPathStack = new NavPathStack();
build() {
Navigation(this.stack) {
// 左栏:分类列表(平板模式下常驻)
CategoryList()
}
.navDestination(AppPageMap)
// Auto 模式:屏幕宽 < 520vp 单栏,>= 520vp 自动双栏
.mode(NavigationMode.Auto)
// 双栏时左栏宽度
.navBarWidth(300)
.navBarWidthRange([240, 400])
.hideTitleBar(true)
}
}
| 模式 | 说明 |
|---|---|
NavigationMode.Stack | 始终单栏(手机常用) |
NavigationMode.Split | 始终双栏(横屏平板) |
NavigationMode.Auto | 自动根据屏幕宽度切换 |
3.7 自定义转场动画
Navigation 支持完全自定义的转场效果:
// 入场动画(从右侧滑入)
stack.pushPathByName('DetailPage', param);
// 在 NavDestination 中配置动画
NavDestination() {
// ...
}
.customNavContentTransition((from, to, operation) => {
// operation: NavigationOperation.PUSH / POP / REPLACE
const isForward = operation === NavigationOperation.PUSH;
return {
onTransitionEnd: (isSuccess: boolean) => {},
timeout: 1000,
transition: (proxy: NavigationTransitionProxy) => {
// 自定义动画逻辑
animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
proxy.finishTransition();
});
}
};
})
3.8 NavDestination 生命周期
NavDestination 有独立的生命周期,比 Router 页面更精细:
NavDestination() {
// ...
}
.onReady((ctx: NavDestinationContext) => {
// 页面创建完成,可获取参数和路由栈
const param = ctx.pathInfo.param;
const stack = ctx.pathStack;
})
.onShown(() => {
// 页面出现在前台(包括从下级页面返回)
console.info('页面可见,刷新数据');
})
.onHidden(() => {
// 页面被遮挡(跳转到下级页面时)
console.info('页面不可见,暂停动画');
})
.onBackPressed(() => {
// 拦截返回键,返回 true 表示自行处理(不执行默认返回)
if (this.hasUnsavedChanges) {
this.showSaveDialog();
return true;
}
return false; // false = 执行默认返回行为
})
四、Router vs Navigation 对比总结
4.1 参数传递对比
Router(类型不安全)
// 发送方
router.pushUrl({
url: 'pages/Detail',
params: { id: '123' } // Object 类型,无类型约束
});
// 接收方(需要手动断言,容易出错)
const params = router.getParams() as { id: string };
Navigation(强类型)
// 发送方
interface DetailParam { id: string; }
stack.pushPathByName('DetailPage', { id: '123' } as DetailParam);
// 接收方(通过 @Param 直接接收,完全类型安全)
@ComponentV2
struct DetailPage {
@Param param: DetailParam = { id: '' };
}
4.2 返回值对比
Router(繁琐)
// 返回方(只能夹带在 params 里)
router.back({ url: 'pages/List', params: { refreshed: true } });
// 接收方(需要在 onPageShow 里判断)
onPageShow() {
const p = router.getParams() as { refreshed?: boolean };
if (p?.refreshed) this.refresh();
}
Navigation(优雅)
// 跳转时直接注册回调
stack.pushPathByName('EditPage', param, (popInfo) => {
const result = popInfo.result as { refreshed: boolean };
if (result.refreshed) this.refresh();
});
// 返回方
stack.pop({ refreshed: true });
4.3 适用场景选择
新项目?
└── 是 → 直接用 Navigation ✅
旧项目?
├── 页面不多,维护即可 → 继续用 Router
└── 需要平板适配 / 复杂动画 → 逐步迁移到 Navigation
需要平板双栏布局?
└── 必须用 Navigation(NavigationMode.Auto)
需要拦截返回键?
├── Router → onBackPress() 只能全局处理
└── Navigation → NavDestination.onBackPressed() 精细控制
页面层级深(3层以上)?
└── 推荐 Navigation(NavPathStack 管理更清晰)
五、混用方案(过渡期)
如果项目中已有大量 Router 代码,短期内可以混用,但需注意:
// 在 Navigation 体系中调用 Router 跳转到旧页面
import { router } from '@ohos.router';
@ComponentV2
struct SomePage {
@Consumer() stack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Button('跳转旧页面(Router)')
.onClick(() => {
// 新旧混用,谨慎使用
router.pushUrl({ url: 'pages/OldPage' });
})
}
}
}
⚠️ 混用注意事项:
- Router 和 Navigation 各自维护独立的路由栈,
router.back()不会影响NavPathStack - 混用期间动画可能不一致
- 建议新功能全部用 Navigation,旧页面逐步迁移
六、常见坑点
坑 1:NavDestination 不写 @Param 导致参数丢失
// ❌ 错误:用普通属性接收,首次渲染时参数可能未初始化
@ComponentV2
struct DetailPage {
param: DetailParam = { id: '' }; // 没有 @Param
}
// ✅ 正确:必须用 @Param 装饰器
@ComponentV2
struct DetailPage {
@Param param: DetailParam = { id: '' };
}
坑 2:路由栈没有用 @Provider 导致子组件拿不到
// ❌ 错误:普通属性不会被 @Consumer 接收
struct Index {
private stack: NavPathStack = new NavPathStack(); // 普通属性
}
// ✅ 正确:用 @Provider 使子组件可以 @Consumer 接收
struct Index {
@Provider() stack: NavPathStack = new NavPathStack();
}
坑 3:多个 Navigation 嵌套导致路由混乱
// ❌ 错误:Index 和 DetailPage 各自有 Navigation,会产生两个路由栈
struct Index {
build() {
Navigation(this.stack) {
// ...
}
}
}
struct DetailPage {
build() {
NavDestination() {
Navigation(anotherStack) { // ❌ 多余的嵌套 Navigation
// ...
}
}
}
}
// ✅ 正确:全局只有一个根 Navigation,子页面用 NavDestination
坑 4:忘记在 navDestination 中注册页面
// ❌ 错误:跳转后白屏
Navigation(this.stack) { /* ... */ }
// 没有 .navDestination(pageMap)
// ✅ 正确:必须绑定 pageMap
Navigation(this.stack) { /* ... */ }
.navDestination(pageMap)
七、总结
| 场景 | 推荐方案 |
|---|---|
| 新项目 | Navigation(全面使用) |
| 旧项目维护 | Router(不动现有代码) |
| 需要平板/折叠屏适配 | Navigation(Auto 模式) |
| 需要强类型参数 | Navigation(@Param) |
| 需要优雅的返回值 | Navigation(pop 回调) |
| 需要精细控制生命周期 | Navigation(onShown/onHidden) |
| 简单的几个页面跳转 | Router 或 Navigation 均可 |
一句话总结: Router 是「跳页面」,Navigation 是「管状态」——Navigation 把整个应用的页面流转统一纳入 NavPathStack 管理,是鸿蒙官方力推的现代导航方案,新项目直接用就对了。
到此这篇关于鸿蒙Router与Navigation组件导航的文章就介绍到这了,更多相关鸿蒙Router与Navigation组件导航内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
浅析mmdetection在windows10系统环境中搭建过程
这篇文章主要介绍了mmdetection在windows10系统环境中搭建过程,本文图文并茂通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下2020-01-01
JetBrains发布java代码质量检测工具Qodana早期预览版
这篇文章主要介绍了JetBrains发布java代码质量检测工具Qodana早期预览版,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2021-01-01
MobaXterm详细使用图文教程(MobaXterm连接Linux服务器)
这篇文章主要介绍了MobaXterm详细使用教程,介绍一下如何设置并用MobaXterm来连接Linux服务器,本文介绍了三种连接方式:SSH,FTP,serial,以及几个有用的设置和命令,需要的朋友可以参考下2023-05-05
win10安装Anaconda+tensorflow2.0-CPU+Pycharm的图文教程
本文通过图文并茂的形式给大家介绍了win10安装Anaconda+tensorflow2.0-CPU+Pycharm的教程,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下2019-12-12


最新评论