从 Router 切换到 Navigation,一篇搞定鸿蒙页面导航升级
《从Router到Navigation:鸿蒙开发导航方案升级指南》摘要: 本文详细对比鸿蒙开发中Router与Navigation两种导航方案,重点介绍如何从Router迁移到官方推荐的Navigation。
在鸿蒙开发中,页面导航是个绕不开的话题。之前大家常用的 Router 虽然简单,但功能有限。现在官方更推荐用 Navigation,它支持更丰富的动效、一次开发多端部署,栈操作也更灵活。今天就手把手教你从 Router 迁移到 Navigation,每个知识点都配代码,保证一看就懂。
一、页面结构:从单一页面到导航容器
重点总结:Router 是独立页面集合,Navigation 是 "导航容器 + 子页面" 结构;Router 需在 main_page.json 注册,Navigation 要配置 route_map.json。
Router 的页面结构很简单,每个页面都是 @Entry 修饰的 Component,然后在 main_page.json 里列出来就行:
// Router的页面配置:main_page.json
{
"src": [
"pages/Index",
"pages/pageOne",
"pages/pageTwo"
]
}
对应的页面代码长这样,直接用 @Entry:
// Router页面示例:index.ets
import { router } from '@kit.ArkUI';
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
Button('跳转到pageOne')
.onClick(() => {
router.pushUrl({ url: 'pages/pageOne' })
})
}
}
}
}
而 Navigation 采用 "导航容器 + 子页面" 的结构,导航页用 Navigation 组件,子页面用 NavDestination。子页面要在 route_map.json 里配置:
// Navigation的页面配置:route_map.json
{
"routerMap": [
{
"name": "pageOne",
"pageSourceFile": "src/main/ets/pages/PageOne.ets",
"buildFunction": "PageOneBuilder",
"data": { "description": "this is pageOne" }
}
]
}
Navigation 的导航页代码长这样,注意要创建 NavPathStack 对象:
// Navigation导航页示例:index.ets
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack(); // 页面栈对象
build() {
Navigation(this.pathStack) { // 把栈对象传给导航容器
Column() {
Button('跳转到pageOne')
.onClick(() => {
this.pathStack.pushPathByName('pageOne', null);
})
}
}
.title("Navigation")
.mode(NavigationMode.Stack) // 栈模式
}
}
子页面要用 NavDestination 包裹,还得通过 onReady 获取栈对象:
// Navigation子页面示例:PageOne.ets
@Builder
export function PageOneBuilder() {
PageOne();
}
@Component
export struct PageOne {
pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Column() {
Text('This is pageOne')
Button('回到首页')
.onClick(() => {
this.pathStack.clear(); // 清空栈返回首页
})
}
}
.title('PageOne')
.onReady((context) => { // 获取导航栈对象
this.pathStack = context.pathStack;
})
}
}
二、路由操作:从全局调用到栈对象操作
重点总结:Router 用全局 router 模块,Navigation 用 NavPathStack 对象;Navigation 的栈操作更丰富,支持按名称 / 索引跳转,还能删除指定页面。
Router 的路由操作全靠 router 模块的方法,比如跳转、返回:
// Router常用操作
import { router } from '@kit.ArkUI';
// 跳转到新页面
router.pushUrl({ url: "pages/pageOne" }, router.RouterMode.Standard);
// 返回上一页
router.back();
// 替换当前页面
router.replaceUrl({ url: "pages/pageOne" });
// 清空页面栈
router.clear();
// 获取栈大小
let size = router.getLength();
Navigation 则通过 NavPathStack 对象实现路由操作,方法更灵活:
// Navigation常用操作
// 跳转到新页面
this.pathStack.pushPath({ name: 'pageOne' });
// 返回上一页
this.pathStack.pop();
// 返回到指定索引页面
this.pathStack.popToIndex(1);
// 返回到指定名称页面
this.pathStack.popToName('pageOne');
// 替换当前页面
this.pathStack.replacePath({ name: 'pageOne' });
// 清空页面栈
this.pathStack.clear();
// 获取栈大小
let size = this.pathStack.size();
// 删除指定名称的所有页面
this.pathStack.removeByName("pageOne");
// 获取所有页面名称
this.pathStack.getAllPathName();
子页面要操作路由,得先拿到 NavPathStack 对象,推荐这两种方式:
- 通过 onReady 回调(简单直接):
@Component
export struct PageOne {
pathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
// 页面内容
}
.onReady((context) => {
this.pathStack = context.pathStack; // 从上下文获取
})
}
}
- 通过全局 AppStorage(跨组件方便):
// 导航页设置
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack();
aboutToAppear() {
AppStorage.setOrCreate("pathStack", this.pathStack); // 存入全局
}
build() {
Navigation(this.pathStack) { ... }
}
}
// 子页面获取
@Component
export struct PageOne {
pathStack: NavPathStack = AppStorage.get("pathStack") as NavPathStack;
}
三、生命周期:从页面方法到组件事件
重点总结:Router 是 @Entry 页面的四个生命周期方法,Navigation 是 NavDestination 的八个事件回调;两者可以大致对应,但 Navigation 更精细。
Router 的生命周期很简单,直接在 @Entry 组件里写这四个方法:
// Router生命周期
@Entry
@Component
struct Index {
// 页面创建后
aboutToAppear(): void {
console.log('页面创建了');
}
// 页面销毁前
aboutToDisappear(): void {
console.log('页面要没了');
}
// 页面显示时
onPageShow(): void {
console.log('页面显示了');
}
// 页面隐藏时
onPageHide(): void {
console.log('页面藏起来了');
}
build() { ... }
}
Navigation 把生命周期放到了 NavDestination 的事件里,细分得更细:
// Navigation生命周期
@Component
struct PageOne {
build() {
NavDestination() {
// 页面内容
}
.onWillAppear(() => {
// 准备显示(还没渲染)
console.log('准备显示了');
})
.onAppear(() => {
// 已经显示(完成渲染)
console.log('显示完成');
})
.onWillHide(() => {
// 准备隐藏
console.log('准备隐藏了');
})
.onHidden(() => {
// 已经隐藏
console.log('隐藏完成');
})
.onWillDisappear(() => {
// 准备销毁
console.log('准备销毁了');
})
.onDisAppear(() => {
// 已经销毁
console.log('销毁完成');
})
}
}
简单对应关系:
- onPageShow → onAppear
- onPageHide → onHidden
- aboutToAppear → onWillAppear
- aboutToDisappear → onWillDisappear
四、转场动画:从页面过渡到组件动画
重点总结:Router 用 pageTransition 定义转场,Navigation 用 customNavContentTransition;共享元素转场 Router 用 sharedTransition,Navigation 用 geometryTransition。
Router 的自定义转场动画通过 pageTransition 方法实现:
// Router自定义转场
@Entry
@Component
struct PageOne {
pageTransition() {
// 入场动画:从右侧滑入
SlideEffect.Right.in({ duration: 500 })
// 出场动画:向左侧滑出
SlideEffect.Left.out({ duration: 500 })
}
build() { ... }
}
Navigation 的转场动画通过 customNavContentTransition 配置:
// Navigation自定义转场
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pathStack) {
// 页面内容
}
.mode(NavigationMode.Stack)
// 自定义转场动画
.customNavContentTransition((builder, isPush: boolean) => {
if (isPush) {
// 入场:淡入+缩放
builder
.opacity(0)
.scale({ x: 0.8, y: 0.8 })
.transition({
type: TransitionType.All,
duration: 500,
curve: Curve.EaseOut
})
} else {
// 出场:淡出+缩小
builder
.opacity(1)
.scale({ x: 1, y: 1 })
.transition({
type: TransitionType.All,
duration: 500,
curve: Curve.EaseIn
})
}
})
}
}
共享元素转场(页面间元素平滑过渡)在 Router 里用 sharedTransition 属性:
// Router共享元素转场
// 页面A
Image('logo')
.sharedTransition('logo', { duration: 500 })
// 页面B
Image('logo')
.sharedTransition('logo', { duration: 500 })
Navigation 里则用 geometryTransition 实现,效果更流畅:
// Navigation共享元素转场
// 页面A
Image('logo')
.geometryTransition('logo', this.transitionParam)
// 页面B
Image('logo')
.geometryTransition('logo', this.transitionParam)
// 定义过渡参数
private transitionParam: GeometryTransitionParam = {
duration: 500,
curve: Curve.EaseInOut
}
五、跨包路由:从命名路由到直接支持
重点总结:Router 需配置命名路由并手动引入,Navigation 默认支持跨包;Navigation 通过导入组件 + 配置 pageMap 即可实现。
Router 跨包跳转要先在共享包(HAR/HSP)里定义命名路由:
// 共享包里的页面(library/src/main/ets/pages/Index.ets)
@Entry({ routeName: 'myPage' })
@Component
export struct MyComponent {
build() { ... }
}
然后在主包页面里导入并跳转:
// 主包页面跳转
import { router } from '@kit.ArkUI';
import('library/src/main/ets/pages/Index'); // 导入共享包页面
@Entry
@Component
struct Index {
build() {
Text('点击跳转')
.onClick(() => {
router.pushNamedRoute({ name: 'myPage' }); // 跳转到共享包页面
})
}
}
Navigation 跨包更简单,共享包页面只需导出 NavDestination 组件:
// 共享包里的页面(library/src/main/ets/pages/PageInHSP.ets)
@Component
export struct PageInHSP {
build() {
NavDestination() { ... }
}
}
// 共享包入口文件导出
export { PageInHSP } from "./src/main/ets/pages/PageInHSP"
主包页面导入后配置 pageMap 就能跳转:
// 主包页面跳转
import { PageInHSP } from 'library/src/main/ets/pages/PageInHSP';
@Entry
@Component
struct mainPage {
pageStack: NavPathStack = new NavPathStack();
// 定义路由映射表
@Builder pageMap(name: string) {
if (name === 'PageInHSP') {
PageInHSP(); // 关联共享包页面
}
}
build() {
Navigation(this.pageStack) {
Button("跳转到共享包页面")
.onClick(() => {
this.pageStack.pushPath({ name: "PageInHSP" }); // 直接跳转
})
}
.mode(NavigationMode.Stack)
.navDestination(this.pageMap) // 应用映射表
}
}
六、动态路由:从自定义实现到系统支持
重点总结:Router 需手动实现路由表 + 注册 + 跳转流程,Navigation 有两种方案;API 12 + 支持系统路由表,实现模块解耦。
动态路由能解决模块解耦问题,让不同 HAP 之间无需互相依赖就能跳转。
Router 实现动态路由要三步:
- 定义路由表和页面绑定
- 在入口 Ability 注册路由表
- 通过路由名称跳转
Navigation 推荐用系统路由表(API 12+),在共享包的 router_map.json 里配置:
// 共享包的router_map.json
{
"routerMap": [
{
"name": "HspPage",
"pageSourceFile": "src/main/ets/pages/HspPage.ets",
"buildFunction": "HspPageBuilder"
}
]
}
主包无需导入,直接通过名称跳转:
// 主包页面动态跳转
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pathStack) {
Button("跳转到动态页面")
.onClick(() => {
this.pathStack.pushPath({ name: "HspPage" }); // 直接用名称跳转
})
}
.mode(NavigationMode.Stack)
}
}
系统会自动加载对应模块并完成跳转,实现了模块完全解耦。
七、其他实用功能:监听、查询和拦截
重点总结:两者都支持生命周期监听和页面信息查询;Router 需手动实现路由拦截,Navigation 有现成的 setInterception 方法。
生命周期监听
Router 通过 uiObserver 监听页面变化:
import { uiObserver } from '@kit.ArkUI';
// 监听页面更新
uiObserver.on('routerPageUpdate', this.getUIContext(), (info) => {
console.log('页面信息:' + JSON.stringify(info));
});
Navigation 监听 NavDestination 变化:
// 在Ability中监听
import { UIObserver } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage) {
windowStage.getMainWindow((err, data) => {
let uiContext = data.getUIContext();
let uiObserver = uiContext.getUIObserver();
// 监听子页面状态
uiObserver.on("navDestinationUpdate",(info) => {
if (info.state === 0) { // 0表示显示
console.log('页面显示:' + info.name);
}
})
})
}
}
页面信息查询
Router 查询当前页面信息:
@Component
struct MyComponent {
aboutToAppear() {
let info = this.queryRouterPageInfo(); // 获取页面信息
console.log('页面ID:' + info?.pageId);
}
}
Navigation 查询子页面信息:
@Component
struct MyComponent {
aboutToAppear() {
let info = this.queryNavDestinationInfo(); // 获取子页面信息
console.log('子页面ID:' + info?.navDestinationId);
}
}
路由拦截
Router 需要自己封装拦截逻辑:
// 封装Router跳转方法
function pushWithInterceptor(url: string) {
// 拦截判断:未登录跳登录页
if (!isLogin() && url !== 'pages/Login') {
router.pushUrl({ url: 'pages/Login' });
return;
}
router.pushUrl({ url });
}
Navigation 直接用 setInterception 方法:
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pathStack) { ... }
.mode(NavigationMode.Stack)
.setInterception((target) => {
// 拦截逻辑:未登录拦截并跳登录页
if (!isLogin() && target.name !== 'Login') {
this.pathStack.pushPath({ name: 'Login' });
return true; // 拦截原跳转
}
return false; // 允许跳转
})
}
}
总结
从 Router 到 Navigation,不只是 API 的变化,更是一种更灵活、更强大的页面管理方式。Navigation 的容器化设计让页面交互更丰富,栈操作更精细,跨包和动态路由支持也更完善。
迁移时可以分步骤进行:先改页面结构,再替换路由操作,最后处理生命周期和动画。按照本文的代码示例一步步改,很快就能完成升级。赶紧试试用 Navigation 重构你的页面导航吧,体验会好很多!
更多推荐



所有评论(0)