Router切换Navigation
架构差异
从ArkUI组件树层级上来看,原先由Router管理的page在页面栈管理节点stage的下面。Navigation作为导航容器组件,可以挂载在单个page节点下,也可以叠加、嵌套。Navigation管理了标题栏、内容区和工具栏,内容区用于显示用户自定义页面的内容,并支持页面的路由能力。Navigation的这种设计上有如下优势:
image
- 接口上显式区分标题栏、内容区和工具栏,实现更加灵活的管理和UX动效能力;
- 显式提供路由容器概念,由开发者决定路由容器的位置,支持在全模态、半模态、弹窗中显示;
- 整合UX设计和一多能力,默认提供统一的标题显示、页面切换和单双栏适配能力;
- 基于通用UIBuilder能力,由开发者决定页面别名和页面UI对应关系,提供更加灵活的页面配置能力;
- 基于组件属性动效和共享元素动效能力,将页面切换动效转换为组件属性动效实现,提供更加丰富和灵活的切换动效;
- 开放了页面栈对象,开发者可以继承,能更好的管理页面显示。
能力对标
业务场景 | 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) | 支持 | 不支持 |
模态嵌套路由 | 支持 | 不支持 |
切换指导
页面结构
Router路由的页面是一个@Entry
修饰的Component,每一个页面都需要在main_page.json
中声明。
// main_page.json
{
"src": [
"pages/Index",
"pages/PageOne",
"pages/PageTwo"
]
}
以下为Router页面的示例:
代码语言:javascript复制// index.ets
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
而基于Navigation的路由页面分为导航页和子页,导航页又叫Navbar,是Navigation包含的子组件,子页是NavDestination包含的子组件。
以下为Navigation导航页的示例:
代码语言:javascript复制// index.ets
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.pathStack) {
Column() {
Button('Push PageOne', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pathStack.pushPathByName('PageOne', null)
})
}.width('100%').height('100%')
}
.title("Navigation")
}
}
以下为Navigation子页的示例:
代码语言:javascript复制// PageOne.ets
@Builder
export function PageOneBuilder() {
PageOne()
}
@Component
export struct PageOne {
pathStack: NavPathStack = new NavPathStack()
build() {
NavDestination() {
Column() {
Button('回到首页', { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
this.pathStack.clear()
})
}.width('100%').height('100%')
}.title('PageOne')
.onReady((context: NavDestinationContext) => {
this.pathStack = context.pathStack
})
}
}
首先在module.json中添加
// 工程配置文件module.json5中配置 {"routerMap": "$profile:route_map"}
代码语言:javascript复制"routerMap": "$profile:route_map",
然后每个子页也需要配置到系统配置文件route_map.json
中(参考系统路由配置[1]):
//新建
// route_map.json
{
"routerMap": [
{
"name": "PageOne",
"pageSourceFile": "src/main/ets/pages/PageOne.ets",
"buildFunction": "PageOneBuilder",
"data": {
"description": "this is PageOne"
}
}
]
}
image-20240618210827933
路由操作
代码语言:javascript复制// push page
this.pathStack.pushPath({ name: 'PageOne' })
// pop page
this.pathStack.pop()
this.pathStack.popToIndex(1)
this.pathStack.popToName('PageOne')
// replace page
this.pathStack.replacePath({ name: 'PageOne' })
// clear all page
this.pathStack.clear()
// 获取页面栈大小
let size = this.pathStack.size()
// 删除栈中name为PageOne的所有页面
this.pathStack.removeByName("PageOne")
// 删除指定索引的页面
this.pathStack.removeByIndexes([1,3,5])
// 获取栈中所有页面name集合
this.pathStack.getAllPathName()
// 获取索引为1的页面参数
this.pathStack.getParamByIndex(1)
// 获取PageOne页面的参数
this.pathStack.getParamByName("PageOne")
// 获取PageOne页面的索引集合
this.pathStack.getIndexByName("PageOne")
...
Navigation作为组件,子页面想要做路由需要拿到Navigation持有的页面栈对象NavPathStack,可以通过如下几种方式获取:
通过全局的AppStorage
接口设置获取;
@Entry
@Component
struct Index {
pathStack: NavPathStack = new NavPathStack()
// 全局设置一个NavPathStack
aboutToAppear(): void {
AppStorage.setOrCreate("PathStack", this.pathStack)
}
build() {
Navigation(this.pathStack) {
...
}.width('100%').height('100%')
}
.title("Navigation")
}
}
// Navigation子页面
@Component
export struct PageOne {
// 子页面中获取全局的NavPathStack
pathStack: NavPathStack = AppStorage.get("PathStack") as NavPathStack
build() {
NavDestination() {
...
}
.title("PageOne")
}
}
通过自定义组件查询接口获取(参考自定义组件方法[2]);
代码语言:javascript复制import observer from '@ohos.arkui.observer';
// 子页面中的自定义组件
@Component
struct CustomNode {
pathStack : NavPathStack = new NavPathStack()
aboutToAppear() {
// query navigation info
let navigationInfo : NavigationInfo = this.queryNavigationInfo() as NavigationInfo
this.pathStack = navigationInfo.pathStack;
}
build() {
Row() {
Button('跳转到PageTwo')
.onClick(()=>{
this.pathStack.pushPath({ name: 'pageTwo' })
})
}
}
}
生命周期
Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。 具体生命周期描述请参考:Navigation生命周期[3]
代码语言:javascript复制@Component
struct PageOne {
aboutToDisappear() {
}
aboutToAppear() {
}
build() {
NavDestination() {
...
}
.onWillAppear(()=>{
})
.onAppear(()=>{
})
.onWillShow(()=>{
})
.onShown(()=>{
})
.onWillHide(()=>{
})
.onHidden(()=>{
})
.onWillDisappear(()=>{
})
.onDisAppear(()=>{
})
}
}
转场动画
Navigation提供了系统的转场动画也提供了自定义转场的能力。
Navigation作为路由容器组件,其内部的页面切换动画本质上属于组件跟组件之间的属性动画,可以通过Navigation中的customNavContentTransition[4]事件提供自定义转场动画的能力,具体实现可以参考如下指导: Navigation自定义转场动画[5](注意:Dialog类型的页面当前没有转场动画)
共享元素转场
Navigation提供了共享元素一镜到底的转场能力,需要配合geometryTransition
属性,在子页面(NavDestination)之间切换时,可以实现共享元素转场,具体可以参考如下指导:Navigation共享元素转场动画[6]
跨包路由
Navigation作为路由组件,默认支持跨包跳转。
- 从HSP(HAR)中完成自定义组件(需要跳转的目标页面)开发,将自定义组件申明为export;
@Component
export struct PageInHSP {
build() {
NavDestination() {
...
}
}
}
2.在HSP(HAR)的index.ets中导出组件
代码语言:javascript复制export { PageInHSP } from "./src/main/ets/pages/PageInHSP"
3.配置好HSP(HAR)的项目依赖后,在mainPage中导入自定义组件,并添加到pageMap中,即可正常调用。
代码语言:javascript复制// 1.导入跨包的路由页面
import { PageInHSP } from 'library/src/main/ets/pages/PageInHSP'
@Entry
@Component
struct mainPage {
pageStack: NavPathStack = new NavPathStack()
@Builder pageMap(name: string) {
if (name === 'PageInHSP') {
// 2.定义路由映射表
PageInHSP()
}
}
build() {
Navigation(this.pageStack) {
Button("Push HSP Page")
.onClick(() => {
// 3.跳转到Hsp中的页面
this.pageStack.pushPath({ name: "PageInHSP"});
})
}
.navDestination(this.pageMap)
}
}
以上是通过静态依赖的形式完成了跨包的路由,在大型的项目中一般跨模块的开发需要解耦,那就需要依赖动态路由的能力。
动态路由
动态路由设计的目的是解决多个产品(Hap)之间可以复用相同的业务模块,各个业务模块之间解耦(模块之间跳转通过路由表跳转,不需要互相依赖)和路由功能扩展整合。
业务特性模块对外暴露的就是模块内支持完成具体业务场景的多个页面的集合;路由管理就是将每个模块支持的页面都用统一的路由表结构管理起来。 当产品需要某个业务模块时,就会注册对应的模块的路由表。
动态路由的优势:
- 路由定义除了跳转的URL以外,可以丰富的配置任意扩展信息,如横竖屏默认模式,是否需要鉴权等等,做路由跳转时的统一处理。
- 给每个路由设置一个名字,按照名称进行跳转而不是ets文件路径。
- 页面的加载可以使用动态Import(按需加载),防止首个页面加载大量代码导致卡顿。
Router实现动态路由主要有下面三个过程:
- 定义过程: 路由表定义新增路由 -> 页面文件绑定路由名称(装饰器) -> 加载函数和页面文件绑定(动态import函数)
- 定义注册过程: 路由注册(可在入口ability中按需注入依赖模块的路由表)。
- 跳转过程: 路由表检查(是否注册过对应路由名称) -> 路由前置钩子(路由页面加载-动态Import) -> 路由跳转 -> 路由后置钩子(公共处理,如打点)。
Navigation实现动态路由有如下两种实现方案:
方案一: 自定义路由表
基本实现跟上述Router动态路由类似
- 开发者自定义路由管理模块,各个提供路由页面的模块均依赖此模块;
- 构建Navigation组件时,将NavPactStack注入路由管理模块,路由管理模块对NavPactStack进行封装,对外提供路由能力;
- 各个路由页面不再提供组件,转为提供@build封装的构建函数,并再通过WrappedBuilder封装后,实现全局封装;
- 各个路由页面将模块名称、路由名称、WrappedBuilder封装后构建函数注册如路由模块。
- 当路由需要跳转到指定路由时,路由模块完成对指定路由模块的动态导入,并完成路由跳转。
具体的构建过程,可以参考开源工程:Navigation动态路由示例[7]。
方案二: 系统路由表
从API version 12版本开始,Navigation支持系统跨模块的路由表方案,整体设计是将路由表方案下沉到系统中管理,即在需要路由的各个业务模块(HSP/HAR)中独立配置router_map.json
文件,在触发路由跳转时,应用只需要通过NavPactStack
进行路由跳转,此时系统会自动完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块解耦。 具体可参考文档:Navigation系统路由[8]
生命周期监听
Navigation同样可以通过在observer中实现注册监听。
代码语言:javascript复制export default class EntryAbility extends UIAbility {
...
onWindowStageCreate(windowStage: window.WindowStage): void {
...
windowStage.getMainWindow((err: BusinessError, data) => {
...
windowClass = data;
// 获取UIContext实例。
let uiContext: UIContext = windowClass.getUIContext();
// 获取UIObserver实例。
let uiObserver : UIObserver = uiContext.getUIObserver();
// 注册DevNavigation的状态监听.
uiObserver.on("navDestinationUpdate",(info) => {
// NavDestinationState.ON_SHOWN = 0, NavDestinationState.ON_HIDE = 1
if (info.state == 0) {
// NavDestination组件显示时操作
console.info('page ON_SHOWN:' info.name.toString());
}
})
})
}
}
页面信息查询
为了实现页面内自定义组件跟页面解耦,自定义组件中提供了全局查询页面信息的接口。
Navigation通过queryNavDestinationInfo[9]接口查询当前自定义组件所在的NavDestination的信息,其返回值包含如下几个属性,其中navDestinationId是页面的唯一标识符:
名称 | 类型 | 必填 | 说明 |
---|---|---|---|
navigationId | ResourceStr | 是 | 包含NavDestination组件的Navigation组件的id。 |
name | ResourceStr | 是 | NavDestination组件的名称。 |
state | NavDestinationState | 是 | NavDestination组件的状态。 |
index12 | number | 是 | NavDestination在页面栈中的索引。 |
param12 | Object | 否 | NavDestination组件的参数。 |
navDestinationId12 | string | 是 | NavDestination组件的唯一标识ID。 |
import observer from '@ohos.arkui.observer';
@Component
export struct NavDestinationExample {
build() {
NavDestination() {
MyComponent()
}
}
}
@Component
struct MyComponent {
navDesInfo: observer.NavDestinationInfo | undefined
aboutToAppear() {
this.navDesInfo = this.queryNavDestinationInfo();
console.log('get navDestinationInfo: ' JSON.stringify(this.navDesInfo))
}
build() {
// ...
}
}
路由拦截
Navigation提供了setInterception[10]方法,用于设置Navigation页面跳转拦截回调。具体可以参考文档:Navigation路由拦截[11]
关于坚果派
坚果派由坚果等人联合创建,团队拥有12个华为HDE,以及若干其他领域的三十余位万粉博主运营。专注于研究的技术包括HarmonyOS/OpenHarmony,华为自研语言,AI、BlueOS操作系统等。主营业务是面向国内外客户提供新一代信息技术为核心的产品、解决方案和服务。团队聚焦“鸿蒙原生应用”、“智能物联”和“AI赋能”、“人工智能”四大业务领域,依托华为开发者专家等强大的技术团队,以及涵盖需求、开发、测试、运维于一体的综合服务体系,赋能文旅、媒体、社交、家居、消费电子等行业客户,满足客户数字化升级转型的需求,帮助客户实现价值提升。
参考资料
[1]
系统路由配置: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-navigation-navigation.md#系统路由表
[2]
自定义组件方法: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-api.md#querynavigationinfo12
[3]
Navigation生命周期: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-navigation-navigation.md#页面生命周期
[4]
customNavContentTransition: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#customnavcontenttransition11
[5]
Navigation自定义转场动画: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-navigation-navigation.md#自定义转场
[6]
Navigation共享元素转场动画: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-navigation-navigation.md#共享元素转场
[7]
Navigation动态路由示例: https://gitee.com/harmonyos-cases/cases/tree/master/CommonAppDevelopment/feature/routermodule
[8]
Navigation系统路由: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-navigation-navigation.md#系统路由表
[9]
queryNavDestinationInfo: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-api.md#querynavdestinationinfo
[10]
setInterception: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#setinterception12
[11]
Navigation路由拦截: https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-navigation-navigation.md#路由拦截