在鸿蒙开发中,页面导航是个绕不开的话题。之前大家常用的 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 对象,推荐这两种方式:

  1. 通过 onReady 回调(简单直接):
@Component
export struct PageOne {
  pathStack: NavPathStack = new NavPathStack();
  build() {
    NavDestination() {
      // 页面内容
    }
    .onReady((context) => {
      this.pathStack = context.pathStack; // 从上下文获取
    })
  }
}

  1. 通过全局 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 实现动态路由要三步:

  1. 定义路由表和页面绑定
  2. 在入口 Ability 注册路由表
  3. 通过路由名称跳转

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 重构你的页面导航吧,体验会好很多!

Logo

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

更多推荐