在ArkUI中,路由的导航切换有两种类型:页面跳转、组件跳转。

路由类型

页面路由 @ohos.router(不推荐)

页面路由指在应用程序中实现不同页面之间的跳转和数据传递。
Router模块通过不同的url地址,可以方便地进行页面路由,轻松地访问不同的页面,有时还需要将数据从一个页面传递到另一个页面。

页面跳转

Router模块提供了两种跳转模式,分别是 router.pushUrlrouter.replaceUrl。这两种模式决定了目标页面是否会替换当前页。

  • router.pushUrl:目标页面不会替换当前页,而是压入 页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用 router.back 方法返回到当前页。
    ps:页面栈的最大容量为32个页面。如果超过这个限制,可以调用router.clear方法清空历史页面栈,释放内存空间。
  • router.replaceUrl:目标页面会替换当前页,并销毁当前页。这样可以释放当前页的资源,并且无法返回到当前页。

同时,Router模块提供了两种实例模式,分别是 StandardSingle。这两种模式决定了目标url是否会对应多个实例。

  • Standard:多实例模式。(默认)
    目标页面会被添加到页面栈顶,无论栈中是否存在相同 url 的页面。
  • Single:单实例模式。
    如果目标页面的 url 已经存在于页面栈中,则会将离栈顶最近的同 url 页面移动到栈顶,该页面成为新建页。
    如果目标页面的 url 在页面栈中不存在同 url 页面,则按照默认的多实例模式进行跳转。

在使用Router相关功能之前,需要在代码中先导入Router模块。

import { promptAction, router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
  • 场景一(不携带参数):
    有一个主页(Home)和一个详情页(Detail),希望从主页点击一个商品,跳转到详情页。
    同时,需要保留主页在页面栈中,以便返回时恢复状态。
    根据上面的信息分析结合场景,我们可以使用 pushUrl 方法,并且使用 Standard 实例模式。
import { router } from '@kit.ArkUI';
// 在Home页面中
function onJumpClick(): void {
  router.pushUrl(
  {url: 'pages/Detail'}, // 目标url
  router.RouterMode.Standard, // 实例模式
  (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  });
}
  • 场景二(不携带参数):
    有一个登录页(Login)和一个个人中心页(Profile),希望从登录页成功登录后,跳转到个人中心页。
    同时,销毁登录页,在返回时直接退出应用。
    这种场景下,可以使用 replaceUrl 方法,并且使用 Standard 实例模式。
import { router } from '@kit.ArkUI';
// 在Login页面中
function onJumpClick(): void {
  router.replaceUrl(
  { url: 'pages/Profile' }, // 目标url
  router.RouterMode.Standard, // 实例模式
  (err) => {
    if (err) {
      console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke replaceUrl succeeded.');
  })
}
  • 场景三(不携带参数):
    有一个设置页(Setting)和一个主题切换页(Theme),希望从设置页点击主题选项,跳转到主题切换页。
    同时,需要保证每次只有一个主题切换页存在于页面栈中,在返回时直接回到设置页。
    这种场景下,可以使用 pushUrl 方法,并且使用 Single 实例模式。
import { router } from '@kit.ArkUI';
// 在Setting页面中
function onJumpClick(): void {
  router.pushUrl(
  {url: 'pages/Theme'}, // 目标url
  router.RouterMode.Single, // 实例模式
  (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  });
}
  • 场景四(不携带参数):
    有一个搜索结果列表页(SearchResult)和一个搜索结果详情页(SearchDetail),希望从搜索结果列表页点击某一项结果,跳转到搜索结果详情页。
    同时,如果该结果已经被查看过,则不需要再新建一个详情页,而是直接跳转到已经存在的详情页。
    这种场景下,可以使用 replaceUrl 方法,并且使用 Single 实例模式。
import { router } from '@kit.ArkUI';

// 在SearchResult页面中
function onJumpClick(): void {
  router.replaceUrl(
  {url: 'pages/SearchDetail'}, // 目标url
  router.RouterMode.Single, // 实例模式
  (err) => {
    if (err) {
      console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke replaceUrl succeeded.');
  })
}
  • 页面跳转携带参数:
    如果需要在跳转时传递一些数据给目标页面,则可以在调用Router模块的方法时,添加一个 params 属性,并指定一个对象作为参数。
import { router } from '@kit.ArkUI';

class DataModelInfo {
  age: number = 0;
}

class DataModel {
  id: number = 0;
  info: DataModelInfo|null = null;
}

function onJumpClick(): void {
  // 在Home页面中
  let paramsInfo: DataModel = {
    id: 123,
    info: {
      age: 20
    }
  };
  
  router.pushUrl(
  {
    url: 'pages/Detail', // 目标url
    params: paramsInfo // 添加params属性,传递自定义参数
  }, 
  (err) => {
    if (err) {
      console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`);
      return;
    }
    console.info('Invoke pushUrl succeeded.');
  })
}
  • 获取参数:
    在目标页面中,可以通过调用Router模块的 getParams 方法来获取传递过来的参数。
import { router } from '@kit.ArkUI';

class InfoTmp {
  age: number = 0
}

class RouTmp {
  id: object = () => {
  }
  info: InfoTmp = new InfoTmp()
}

const params: RouTmp = router.getParams() as RouTmp; // 获取传递过来的参数对象
const id: object = params.id // 获取id属性的值
const age: number = params.info.age // 获取age属性的值

页面返回

当用户在一个页面完成操作后,通常需要返回到上一个页面或者指定页面,这就需要用到页面返回功能。
在返回的过程中,可能需要将数据传递给目标页面,这就需要用到数据传递功能。

  • 方式一:router.back()
    返回到上一个页面,
    这种方式会返回到上一个页面,即上一个页面在页面栈中的位置。
    但是,上一个页面 必须存在于页面栈 中才能够返回,否则该方法将无效。
import { router } from '@kit.ArkUI';
router.back();
  • 方式二:router.back( {url: ‘pages/xxx’} )
    返回到指定页面,
    url参数有两种形式:具体的路由路径({ url: ‘pages/Home’ })路由别名({ url: ‘myPage’ }, myPage为返回的命名路由页面别名)
    这种方式可以返回到指定页面,需要指定目标页面的路径。目标页面必须存在于页面栈中才能够返回。
import { router } from '@kit.ArkUI';
// 具体路由
router.back({
  url: 'pages/Home'
});

// 路由别名
router.back({
  url: 'myPage' //myPage为返回的命名路由页面别名
});
  • 方式三:router.back( {url: ‘pages/xxx’}, params:{ xxx : xxx} )
    这种方式不仅可以返回到指定页面,还可以在返回的同时传递自定义参数信息。
    这些参数信息可以在目标页面中通过调用 router.getParams 方法进行获取和解析。
import { router } from '@kit.ArkUI';
// 具体路由
router.back({
  url: 'pages/Home',
  params: {
    info: '来自Home页'
  }
});
// 路由别名
router.back({
  url: 'myPage', //myPage为返回的命名路由页面别名
  params: {
    info: '来自Home页'
  }
});

ps:router.getParams

@Entry
@Component
struct Home {
  @State message: string = 'Hello World';

  onPageShow() {
    const params = this.getUIContext().getRouter().getParams() as Record<string, string>; // 获取传递过来的参数对象
    if (params) {
      const info: string = params.info as string; // 获取info属性的值
    }
  }
  ...
}

// 直接使用router可能导致实例不明确的问题,
// 建议使用 getUIContext 获取 UIContext 实例,并使用 getRouter 获取绑定实例的 router。

当使用router.back方法返回到指定页面时,原栈顶页面(包括)指定页面(不包括)之间的所有页面栈都将从栈中弹出并销毁
另外,如果使用router.back方法返回到原来的页面,原页面不会被重复创建,因此使用@State声明的变量不会重复声明,也不会触发页面的aboutToAppear生命周期回调。如果需要在原页面中使用返回页面传递的自定义参数,可以在需要的位置进行参数解析。例如,在onPageShow生命周期回调中进行参数解析。

页面返回前增加一个询问框

  • 系统默认询问框
    为了实现这个功能,可以使用页面路由Router模块提供的两个方法:router.showAlertBeforeBackPagerouter.back 来实现这个功能
import { router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// 定义一个返回按钮的点击事件处理函数
function onBackClick(): void {
  // 调用router.showAlertBeforeBackPage()方法,设置返回询问框的信息
  try {
    router.showAlertBeforeBackPage({
      message: '您还没有完成支付,确定要返回吗?' // 设置询问框的内容
    });
  } catch (err) {
    let message = (err as BusinessError).message
    let code = (err as BusinessError).code
    console.error(`Invoke showAlertBeforeBackPage failed, code is ${code}, message is ${message}`);
  }
  // 调用router.back()方法,返回上一个页面
  router.back();
}

// router.showAlertBeforeBackPage方法接收一个对象作为参数,该对象包含以下属性:

// message:string类型,表示询问框的内容。

// 如果调用成功,则会在目标界面开启页面返回询问框;如果调用失败,则会抛出异常,
// 并通过err.code和err.message获取错误码和错误信息。

// 当用户点击“返回”按钮时,会弹出确认对话框,询问用户是否确认返回。
// 选择“取消”将停留在当前页目标页面;选择“确认”将触发router.back方法,并根据参数决定如何执行跳转。
  • 自定义询问框
    自定义询问框的方式,可以使用弹窗 promptAction.showDialog 或者自定义弹窗实现。这样可以让应用界面与系统默认询问框有所区别,提高应用的用户体验度。
// 在事件回调中,调用弹窗的promptAction.showDialog方法:

import { promptAction, router } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

function onBackClick() {
  // 弹出自定义的询问框
  promptAction.showDialog({
    message: '您还没有完成支付,确定要返回吗?',
    buttons: [
      {
        text: '取消',
        color: '#FF0000'
      },
      {
        text: '确认',
        color: '#0099FF'
      }
    ]
  }).then((result:promptAction.ShowDialogSuccessResponse) => {
    if (result.index === 0) {
      // 用户点击了“取消”按钮
      console.info('User canceled the operation.');
    } else if (result.index === 1) {
      // 用户点击了“确认”按钮
      console.info('User confirmed the operation.');
      // 调用router.back()方法,并根据参数决定如何执行跳转
      router.back();
    }
  }).catch((err:Error) => {
    let message = (err as BusinessError).message
    let code = (err as BusinessError).code
    console.error(`Invoke showDialog failed, code is ${code}, message is ${message}`);
  })
}

命名路由

在开发中为了跳转到共享包 HAR 或者 HSP 中的页面(即共享包中路由跳转),可以使用 router.pushNamedRoute 来实现。
在想要跳转到的共享包 HAR 或者 HSP 页面里,给 @Entry 修饰的自定义组件 EntryOptions 命名:

// library/src/main/ets/pages/Index.ets
// library为新建共享包自定义的名字
@Entry({ routeName: 'myPage' })
@Component
export struct MyComponent {
  build() {
    Row() {
      Column() {
        Text('Library Page')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
    }
    .height('100%')
  }
}

配置成功后需要在跳转的页面中引入命名路由的页面:

import { BusinessError } from '@kit.BasicServicesKit';
import '@ohos/library/src/main/ets/pages/Index';  // 引入共享包中的命名路由页面
@Entry
@Component
struct Index {
  build() {
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      Text('Hello World')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20 })
        .backgroundColor('#ccc')
        .onClick(() => { // 点击跳转到其他共享包中的页面
          try {
            this.getUIContext().getRouter().pushNamedRoute({
              name: 'myPage',
              params: {
                data1: 'message',
                data2: {
                  data3: [123, 456, 789]
                }
              }
            })
          } catch (err) {
            let message = (err as BusinessError).message
            let code = (err as BusinessError).code
            console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`);
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

使用命名路由方式跳转时,需要在当前应用包的oh-package.json5文件中配置依赖。

"dependencies": {
   "@ohos/library": "file:../library",
   ...
}

组件导航 Navigation(推荐)

Navigation 是路由容器组件,一般作为首页的根容器,包括 单栏(Stack)、分栏(Split)和自适应(Auto) 三种显示模式。

Navigation组件适用于模块内和跨模块的路由切换,一次开发,多端部署场景。通过组件级路由能力实现更加自然流畅的转场体验,并提供多种标题栏样式来呈现更好的标题和内容联动效果。在不同尺寸的设备上,Navigation组件能够自适应显示大小,自动切换分栏展示效果。

Navigation组件 主要包含​ 导航页(NavBar)子页(NavDestination)导航页(NavBar)标题栏(Titlebar,包含菜单栏menu)、内容区(Navigation子组件)工具栏(Toolbar) 组成,其中导航页可以通过 hideNavBar 属性进行隐藏,导航页不存在页面栈中,导航页和子页,以及子页之间可以通过路由操作进行切换。

在API Version 9上,需要配合NavRouter组件实现页面路由,从API Version 10开始,推荐使用NavPathStack实现页面路由。

设置页面显示模式

Navigation组件通过mode属性设置页面的显示模式。

  • 自适应模式 Navigation() { … }.mode(NavigationMode.Auto)
    Navigation组件默认为自适应模式,此时mode属性为NavigationMode.Auto。自适应模式下,当页面宽度大于等于一定阈值( API version 9及以前:520vp,API version 10及以后:600vp )时,Navigation组件采用分栏模式,反之采用单栏模式。

  • 单页面模式 Navigation() { … }.mode(NavigationMode.Stack)
    在这里插入图片描述

  • 分栏模式 Navigation() { … }.mode(NavigationMode.Split)
    在这里插入图片描述

设置标题栏模式

标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件 通过 titleMode 属性设置标题栏模式。
Navigation() { ... }.titleMode(NavigationTitleMode.Mini / Full)

  • Mini模式: 普通型标题栏,用于一级页面不需要突出标题的场景。
    在这里插入图片描述

  • Full模式: 强调型标题栏,用于一级页面需要突出标题的场景。
    在这里插入图片描述

设置菜单栏

菜单栏位于Navigation组件的右上角,开发者可以通过menus属性进行设置。menus支持Array<NavigationMenuItem>CustomBuilder两种参数类型。使用Array<NavigationMenuItem>类型时,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。

let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}
// 图片可以引用resources中的资源
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "resources/base/media/ic_public_highlights.svg", 'action': ()=> {}}

// 设置三个图标的菜单栏
Navigation() {
  // ...
}
.menus([TooTmp, TooTmp, TooTmp])

在这里插入图片描述

let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}

// 设置四个图标的菜单栏
Navigation() {
  // ...
}
.menus([TooTmp, TooTmp, TooTmp, TooTmp])

在这里插入图片描述

设置工具栏

工具栏位于 Navigation组件 的底部,开发者可以通过toolbarConfiguration属性进行设置。

let TooTmp: ToolbarItem = {'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}}
let TooBar: ToolbarItem[] = [TooTmp,TooTmp,TooTmp]
Navigation() {
  // ...
}
.toolbarConfiguration(TooBar)

在这里插入图片描述

路由操作

Navigation路由相关的操作都是基于页面栈NavPathStack提供的方法进行,每个Navigation都需要创建并传入一个NavPathStack对象,用于管理页面。主要涉及页面跳转页面返回页面替换页面删除参数获取路由拦截等功能。

从API version 12开始,页面栈允许被继承。开发者可以在派生类中自定义属性和方法,也可以重写父类的方法。派生类对象可以替代基类NavPathStack对象使用。

@Entry
@Component
struct Index {
  // 创建一个页面栈对象并传入Navigation
  pageStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pageStack) {
    }
    .title('Main')
  }
}
页面跳转

普通跳转,通过页面的name去跳转,并可以携带param

this.pageStack.pushPath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.pushPathByName("PageOne", "PageOne Param")

带返回回调的跳转,跳转时添加onPop回调,能在页面出栈时获取返回信息,并进行处理

this.pageStack.pushPathByName('PageOne', "PageOne Param", (popInfo) => {
  console.log('Pop page name is: ' + popInfo.info.name + ', result: ' + JSON.stringify(popInfo.result))
});

带错误码的跳转,跳转结束会触发异步回调,返回错误码信息

this.pageStack.pushDestinationByName('PageOne', "PageOne Param")
.catch((error: BusinessError) => {
  console.error(`Push destination failed, error code = ${error.code}, error.message = ${error.message}.`);
}).then(() => {
  console.info('Push destination succeed.');
});
页面返回

NavPathStack通过Pop相关接口去实现页面返回功能。

// 返回到上一页
this.pageStack.pop()
// 返回到上一个PageOne页面
this.pageStack.popToName("PageOne")
// 返回到索引为1的页面
this.pageStack.popToIndex(1)
// 返回到根首页(清除栈中所有页面)
this.pageStack.clear()
页面替换

NavPathStack通过Replace相关接口去实现页面替换功能。

// 将栈顶页面替换为PageOne
this.pageStack.replacePath({ name: "PageOne", param: "PageOne Param" })
this.pageStack.replacePathByName("PageOne", "PageOne Param")
页面删除

NavPathStack通过Remove相关接口去实现删除页面栈中特定页面的功能。

// 删除栈中name为PageOne的所有页面
this.pageStack.removeByName("PageOne")
// 删除指定索引的页面
this.pageStack.removeByIndexes([1,3,5])
参数获取

NavPathStack通过Get相关接口去获取页面的一些参数。

// 获取栈中所有页面name集合
this.pageStack.getAllPathName()
// 获取索引为1的页面参数
this.pageStack.getParamByIndex(1)
// 获取PageOne页面的参数
this.pageStack.getParamByName("PageOne")
// 获取PageOne页面的索引集合
this.pageStack.getIndexByName("PageOne")
路由拦截

NavPathStack提供了setInterception方法,用于设置Navigation页面跳转拦截回调。该方法需要传入一个NavigationInterception对象,该对象包含三个回调函数:
willShow ---------------->页面跳转前回调,允许操作栈,在当前跳转生效、
didShow ---------------->页面跳转后回调,在该回调中操作栈会在下一次跳转生效、
modeChange ---------->Navigation单双栏显示状态发生变更时触发该回调

无论是哪个回调,在进入回调时页面栈都已经发生了变化。

this.pageStack.setInterception({
  willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar",
    operation: NavigationOperation, animated: boolean) => {
    if (typeof to === "string") {
      console.log("target page is navigation home page.");
      return;
    }
    // 将跳转到PageTwo的路由重定向到PageOne
    let target: NavDestinationContext = to as NavDestinationContext;
    if (target.pathInfo.name === 'PageTwo') {
      target.pathStack.pop();
      target.pathStack.pushPathByName('PageOne', null);
    }
  }
})

子页面

NavDestination是Navigation子页面的根容器,用于承载子页面的一些特殊属性以及生命周期等。NavDestination可以设置独立的标题栏和菜单栏等属性,使用方法与Navigation相同。NavDestination也可以通过mode属性设置不同的显示类型,用于满足不同页面的诉求。

页面显示类型
  • 标准类型
    NavDestination组件默认为标准类型,此时mode属性为NavDestinationMode.STANDARD。标准类型的NavDestination的生命周期跟随其在NavPathStack页面栈中的位置变化而改变。
  • 弹窗类型
    NavDestination设置mode为NavDestinationMode.DIALOG弹窗类型,此时整个NavDestination默认透明显示。弹窗类型的NavDestination显示和消失时不会影响下层标准类型的NavDestination的显示和生命周期,两者可以同时显示。
// Dialog NavDestination
@Entry
@Component
 struct Index {
   @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack()

   @Builder
   PagesMap(name: string) {
     if (name == 'DialogPage') {
       DialogPage()
     }
   }

   build() {
     Navigation(this.pageStack) {
       Button('Push DialogPage')
         .margin(20)
         .width('80%')
         .onClick(() => {
           this.pageStack.pushPathByName('DialogPage', '');
         })
     }
     .mode(NavigationMode.Stack)
     .title('Main')
     .navDestination(this.PagesMap)
   }
 }

 @Component
 export struct DialogPage {
   @Consume('NavPathStack') pageStack: NavPathStack;

   build() {
     NavDestination() {
       Stack({ alignContent: Alignment.Center }) {
         Column() {
           Text("Dialog NavDestination")
             .fontSize(20)
             .margin({ bottom: 100 })
           Button("Close").onClick(() => {
             this.pageStack.pop()
           }).width('30%')
         }
         .justifyContent(FlexAlign.Center)
         .backgroundColor(Color.White)
         .borderRadius(10)
         .height('30%')
         .width('80%')
       }.height("100%").width('100%')
     }
     .backgroundColor('rgba(0,0,0,0.5)')
     .hideTitleBar(true)
     .mode(NavDestinationMode.DIALOG)
   }
 }
页面生命周期

Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。
其生命周期大致可分为三类,自定义组件生命周期、通用组件生命周期和自有生命周期。
其中,aboutToAppearaboutToDisappear是自定义组件的生命周期(NavDestination外层包含的自定义组件),OnAppearOnDisappear是组件的通用生命周期。剩下的六个生命周期为NavDestination独有。
在这里插入图片描述

  • aboutToAppear:在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前),允许在该方法中改变状态变量,更改将在后续执行build()函数中生效。
  • onWillAppear:NavDestination创建后,挂载到组件树之前执行,在该方法中更改状态变量会在当前帧显示生效。
  • onAppear:通用生命周期事件,NavDestination组件挂载到组件树时执行。
  • onWillShow:NavDestination组件布局显示之前执行,此时页面不可见(应用切换到前台不会触发)。
  • onShown:NavDestination组件布局显示之后执行,此时页面已完成布局。
  • onWillHide:NavDestination组件触发隐藏之前执行(应用切换到后台不会触发)。
  • onHidden:NavDestination组件触发隐藏后执行(非栈顶页面push进栈,栈顶页面pop出栈或应用切换到后台)。
  • onWillDisappear:NavDestination组件即将销毁之前执行,如果有转场动画,会在动画前触发(栈顶页面pop出栈)。
  • onDisappear:通用生命周期事件,NavDestination组件从组件树上卸载销毁时执行。
  • aboutToDisappear:自定义组件析构销毁之前执行,不允许在该方法中改变状态变量。
页面监听和查询

为了方便组件跟页面解耦,在NavDestination子页面内部的自定义组件可以通过全局方法监听或查询到页面的一些状态信息。

  • 页面信息查询
    自定义组件提供queryNavDestinationInfo方法,可以在NavDestination内部查询到当前所属页面的信息,返回值为NavDestinationInfo,若查询不到则返回undefined
 import { uiObserver } from '@kit.ArkUI';

 // NavDestination内的自定义组件
 @Component
 struct MyComponent {
   navDesInfo: uiObserver.NavDestinationInfo | undefined

   aboutToAppear(): void {
     this.navDesInfo = this.queryNavDestinationInfo();
   }

   build() {
       Column() {
         Text("所属页面Name: " + this.navDesInfo?.name)
       }.width('100%').height('100%')
   }
 }
  • 页面状态监听
    通过observer.on(‘navDestinationUpdate’)提供的注册接口可以注册NavDestination生命周期变化的监听,使用方式如下:
uiObserver.on('navDestinationUpdate', (info) => {
     console.info('NavDestination state update', JSON.stringify(info));
 });

也可以注册页面切换的状态回调,能在页面发生路由切换的时候拿到对应的页面信息NavDestinationSwitchInfo,并且提供了UIAbilityContextUIContext不同范围的监听:

 // 在UIAbility中使用
 import { UIContext, uiObserver } from '@kit.ArkUI';

 // callBackFunc 是开发者定义的监听回调函数
 function callBackFunc(info: uiObserver.NavDestinationSwitchInfo) {}
 uiObserver.on('navDestinationSwitch', this.context, callBackFunc);

 // 可以通过窗口的getUIContext()方法获取对应的UIContent
 uiContext: UIContext | null = null;
 uiObserver.on('navDestinationSwitch', this.uiContext, callBackFunc);

页面转场

Navigation默认提供了页面切换的转场动画,通过页面栈操作时,会触发不同的转场效果(Dialog类型的页面默认无转场动画),Navigation也提供了关闭系统转场、自定义转场以及共享元素转场的能力。

关闭转场
  • 全局关闭:Navigation通过NavPathStack中提供的disableAnimation方法可以在当前Navigation中关闭或打开所有转场动画。
pageStack: NavPathStack = new NavPathStack()

aboutToAppear(): void {
  this.pageStack.disableAnimation(true)
}
  • 单词关闭:NavPathStack中提供的Push、Pop、Replace等接口中可以设置animated参数,默认为true表示有转场动画,需要单次关闭转场动画可以置为false,不影响下次转场动画。
pageStack: NavPathStack = new NavPathStack()

this.pageStack.pushPath({ name: "PageOne" }, false)
this.pageStack.pop(false)
自定义转场

Navigation通过customNavContentTransition事件提供自定义转场动画的能力,通过如下三步可以定义一个自定义的转场动画。

  1. 构建一个自定义转场动画工具类CustomNavigationUtils,通过一个Map管理各个页面自定义动画对象CustomTransition,页面在创建的时候将自己的自定义转场动画对象注册进去,销毁的时候解注册;
  2. 实现一个转场协议对象NavigationAnimatedTransition,其中timeout属性表示转场结束的超时时间,默认为1000ms,transition属性为自定义的转场动画方法,开发者要在这里实现自己的转场动画逻辑,系统会在转场开始时调用该方法,onTransitionEnd为转场结束时的回调。
  3. 调用customNavContentTransition方法,返回实现的转场协议对象,如果返回undefined,则使用系统默认转场。
共享元素转场

NavDestination之间切换时可以通过geometryTransition实现共享元素转场。配置了共享元素转场的页面同时需要关闭系统默认的转场动画。

  1. 为需要实现共享元素转场的组件添加geometryTransition属性,id参数必须在两个NavDestination之间保持一致。
// 起始页配置共享元素id
NavDestination() {
Column() {
    // ...
    Image($r('app.media.startIcon'))
    .geometryTransition('sharedId')
    .width(100)
    .height(100)
}
}
.title('FromPage')

// 目的页配置共享元素id
NavDestination() {
Column() {
    // ...
    Image($r('app.media.startIcon'))
    .geometryTransition('sharedId')
    .width(200)
    .height(200)
}
}
.title('ToPage')
  1. 将页面路由的操作,放到animateTo动画闭包中,配置对应的动画参数以及关闭系统默认的转场。
NavDestination() {
Column() {
    Button('跳转目的页')
    .width('80%')
    .height(40)
    .margin(20)
    .onClick(() => {
        this.getUIContext()?.animateTo({ duration: 1000 }, () => {
          this.pageStack.pushPath({ name: 'ToPage' }, false)
        })
    })
}
}
.title('FromPage')

跨包动态路由

通过静态import页面再进行路由跳转的方式会造成不同模块之间的依赖耦合,以及首页加载时间长等问题。

动态路由设计的目的就是为了解决多个模块(HAR/HSP)之间可以复用相同的业务,各个业务模块之间解耦和路由功能扩展整合。
动态路由的优势:

  • 路由定义除了跳转的URL以外,可以丰富的配置扩展信息,如横竖屏默认模式,是否需要鉴权等等,做路由跳转时统一处理。
  • 给每个路由页面设置一个名字,按照名称进行跳转而不是文件路径。
  • 页面的加载可以使用动态Import(按需加载),防止首个页面加载大量代码导致卡顿。

动态路由提供系统路由表和自定义路由表两种方式。

  • 系统路由表相对自定义路由表,使用更简单,只需要添加对应页面跳转配置项,即可实现页面跳转。
  • 自定义路由表使用起来更复杂,但是可以根据应用业务进行定制处理。

支持自定义路由表和系统路由表混用。

系统路由表

从API version 12开始,Navigation支持使用系统路由表的方式进行动态路由。各业务模块(HSP/HAR)中需要独立配置route_map.json文件,在触发路由跳转时,应用只需要通过NavPathStack提供的路由方法,传入需要路由的页面配置名称,此时系统会自动完成路由模块的动态加载、页面组件构建,并完成路由跳转,从而实现了开发层面的模块解耦。其主要步骤如下:

// 1. 在跳转目标模块的配置文件 module.json5 添加路由表配置:
  {
    "module" : {
      "routerMap": "$profile:route_map"
    }
  }
// 2. 添加完路由配置文件地址后,需要在工程 resources/base/profile 中创建 route_map.json 文件。添加如下配置信息:
  {
    "routerMap": [
      {
        "name": "PageOne",
        "pageSourceFile": "src/main/ets/pages/PageOne.ets",
        "buildFunction": "PageOneBuilder",
        "data": {
          "description" : "this is PageOne"
        }
      }
    ]
  }
// 3. 在跳转目标页面中,需要配置入口 Builder 函数,函数名称
//需要和 route_map.json配置文件 中的 buildFunction 保持一致,否则在编译时会报错。
  // 跳转页面入口函数
  @Builder
  export function PageOneBuilder() {
    PageOne()
  }

  @Component
  struct PageOne {
    pathStack: NavPathStack = new NavPathStack()

    build() {
      NavDestination() {
      }
      .title('PageOne')
      .onReady((context: NavDestinationContext) => {
         this.pathStack = context.pathStack
      })
    }
  }
// 4. 通过 pushPathByName 等路由接口进行页面跳转。(注意:此时Navigation中可以不用配置navDestination属性)。
  @Entry
  @Component
  struct Index {
    pageStack : NavPathStack = new NavPathStack();

    build() {
      Navigation(this.pageStack){
      }.onAppear(() => {
        this.pageStack.pushPathByName("PageOne", null, false);
      })
      .hideNavBar(true)
    }
  }
  
自定义路由表

开发者可以通过自定义路由表的方式来实现跨包动态路由

实现方案

  1. 定义页面跳转配置项。
  • 使用资源文件进行定义,通过资源管理**@ohos.resourceManager**在运行时对资源文件解析。
  • 在ets文件中配置路由加载配置项,一般包括路由页面名称(即pushPath等接口中页面的别名),文件所在模块名称(hsp/har的模块名),加载页面在模块内的路径(相对src目录的路径)。
  1. 加载目标跳转页面,通过动态import将跳转目标页面所在的模块在运行时加载, 在模块加载完成后,调用模块中的方法,通过import在模块的方法中加载模块中显示的目标页面,并返回页面加载完成后定义的Builder函数。
  2. 触发页面跳转,在Navigation的navDestination属性执行步骤2中加载的Builder函数,即可跳转到目标页面。

路由实例项目

Navigation系统路由

架构差异

从ArkUI组件树层级上来看,
原先由Router管理的page在页面栈管理节点stage的下面
Navigation作为导航容器组件,可以挂载在单个page节点下,也可以叠加、嵌套。Navigation管理了标题栏、内容区和工具栏,内容区用于显示用户自定义页面的内容,并支持页面的路由能力
在这里插入图片描述
Navigation的这种设计的优势:

  1. 接口上显式区分标题栏、内容区和工具栏,实现更加灵活的管理和UX动效能力;
  2. 显式提供路由容器概念,由开发者决定路由容器的位置,支持在全模态、半模态、弹窗中显示;
  3. 整合UX设计和一多能力,默认提供统一的标题显示、页面切换和单双栏适配能力;
  4. 基于通用UIBuilder能力,由开发者决定页面别名和页面UI对应关系,提供更加灵活的页面配置能力;
  5. 基于组件属性动效和共享元素动效能力,将页面切换动效转换为组件属性动效实现,提供更加丰富和灵活的切换动效;
  6. 开放了页面栈对象,开发者可以继承,能更好的管理页面显示。

能力对比

业务场景 Navigation Router
一多能力 支持,Auto模式自适应单栏跟双栏显示 不支持
跳转指定页面 pushPath & pushDestination pushUrl & pushNameRoute
跳转HSP中页面 支持 支持
跳转HAR中页面 支持 支持
跳转传参 支持 支持
获取指定页面参数 支持 不支持
传参类型 传参为对象形式 传参为对象形式,对象中暂不支持方法变量
跳转结果回调 支持 支持
跳转单例页面 支持 支持
页面返回 支持 支持
页面返回传参 支持 支持
返回指定路由 支持 支持
页面返回弹窗 支持,通过路由拦截实现 showAlertBeforeBackPage
路由替换 replacePath & replacePathByName replaceUrl & replaceNameRoute
路由栈清理 clear clear
清理指定路由 removeByIndexes & removeByName 不支持
转场动画 支持 支持
自定义转场动画 支持 支持,动画类型受限
屏蔽转场动画 支持全局和单次 支持 设置pageTransition方法duration为0
geometryTransition共享元素动画 支持(NavDestination之间共享) 不支持
页面生命周期监听 UIObserver.on(‘navDestinationUpdate’) UIObserver.on(‘routerPageUpdate’)
获取页面栈对象 支持 不支持
路由拦截 支持通过setInercption做路由拦截 不支持
路由栈信息查询 支持 getState() & getLength()
路由栈move操作 moveToTop & moveIndexToTop 不支持
沉浸式页面 支持 不支持,需通过window配置
设置页面标题栏(titlebar)和工具栏(toolbar) 支持 不支持
模态嵌套路由 支持 不支持

具体的两种方式互相转换的方式可以查看对应的官方文档:点击我就到了

到这里为止,简单的ArkUI的使用就结束了,后面就要开始进行ArkTS的学习。

有兴趣的可以持续关注,感谢。

Logo

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

更多推荐