1# Router切换Navigation 2 3鉴于组件导航(Navigation)支持更丰富的动效、一次开发多端部署能力和更灵活的栈操作。本文主要从页面跳转、动效和生命周期等方面介绍如何从Router切换到Navigation。 4 5## 页面结构 6 7Router路由的页面是一个`@Entry`修饰的Component,每一个页面都需要在`main_page.json`中声明。 8 9```json 10// main_page.json 11{ 12 "src": [ 13 "pages/Index", 14 "pages/pageOne", 15 "pages/pageTwo" 16 ] 17} 18``` 19 20以下为Router页面的示例。 21 22```ts 23// index.ets 24import { router } from '@kit.ArkUI'; 25 26@Entry 27@Component 28struct Index { 29 @State message: string = 'Hello World'; 30 31 build() { 32 Row() { 33 Column() { 34 Text(this.message) 35 .fontSize(50) 36 .fontWeight(FontWeight.Bold) 37 Button('router to pageOne', { stateEffect: true, type: ButtonType.Capsule }) 38 .width('80%') 39 .height(40) 40 .margin(20) 41 .onClick(() => { 42 this.getUIContext().getRouter().pushUrl({ 43 url: 'pages/pageOne' // 目标url 44 }, router.RouterMode.Standard, (err) => { 45 if (err) { 46 console.error(`Invoke pushUrl failed, code is ${err.code}, message is ${err.message}`); 47 return; 48 } 49 console.info('Invoke pushUrl succeeded.'); 50 }) 51 }) 52 } 53 .width('100%') 54 } 55 .height('100%') 56 } 57} 58``` 59 60```ts 61// pageOne.ets 62import { router } from '@kit.ArkUI'; 63 64@Entry 65@Component 66struct pageOne { 67 @State message: string = 'This is pageOne'; 68 69 build() { 70 Row() { 71 Column() { 72 Text(this.message) 73 .fontSize(50) 74 .fontWeight(FontWeight.Bold) 75 Button('router back to Index', { stateEffect: true, type: ButtonType.Capsule }) 76 .width('80%') 77 .height(40) 78 .margin(20) 79 .onClick(() => { 80 this.getUIContext().getRouter().back(); 81 }) 82 } 83 .width('100%') 84 } 85 .height('100%') 86 } 87} 88``` 89 90而基于Navigation的路由页面分为导航页和子页,导航页又叫Navbar,是Navigation包含的子组件,子页是NavDestination包含的子组件。 91 92以下为Navigation导航页的示例。 93 94```ts 95// index.ets 96@Entry 97@Component 98struct Index { 99 pathStack: NavPathStack = new NavPathStack() 100 101 build() { 102 Navigation(this.pathStack) { 103 Column() { 104 Button('Push PageOne', { stateEffect: true, type: ButtonType.Capsule }) 105 .width('80%') 106 .height(40) 107 .margin(20) 108 .onClick(() => { 109 this.pathStack.pushPathByName('pageOne', null) 110 }) 111 }.width('100%').height('100%') 112 } 113 .title("Navigation") 114 .mode(NavigationMode.Stack) 115 } 116} 117``` 118以下为Navigation子页的示例。 119 120```ts 121// PageOne.ets 122 123@Builder 124export function PageOneBuilder() { 125 PageOne() 126} 127 128@Component 129export struct PageOne { 130 pathStack: NavPathStack = new NavPathStack() 131 132 build() { 133 NavDestination() { 134 Column() { 135 Button('回到首页', { stateEffect: true, type: ButtonType.Capsule }) 136 .width('80%') 137 .height(40) 138 .margin(20) 139 .onClick(() => { 140 this.pathStack.clear() 141 }) 142 }.width('100%').height('100%') 143 }.title('PageOne') 144 .onReady((context: NavDestinationContext) => { 145 this.pathStack = context.pathStack 146 }) 147 } 148} 149``` 150 151每个子页也需要配置到系统配置文件`route_map.json`中(参考[系统路由表](arkts-navigation-navigation.md#系统路由表))。 152 153```json 154// 工程配置文件module.json5中配置 {"routerMap": "$profile:route_map"} 155// route_map.json 156{ 157 "routerMap": [ 158 { 159 "name": "pageOne", 160 "pageSourceFile": "src/main/ets/pages/PageOne.ets", 161 "buildFunction": "PageOneBuilder", 162 "data": { 163 "description": "this is pageOne" 164 } 165 } 166 ] 167} 168``` 169 170## 路由操作 171 172Router通过`@ohos.router`模块提供的方法来操作页面,建议使用[UIContext](../reference/apis-arkui/js-apis-arkui-UIContext.md#uicontext)中的[getRouter](../reference/apis-arkui/js-apis-arkui-UIContext.md#getrouter)获取[Router](../reference/apis-arkui/js-apis-arkui-UIContext.md#router)实例。 173 174```ts 175// push page 176router.pushUrl({ url:"pages/pageOne", params: null }) 177 178// pop page 179this.getUIContext().getRouter().back({ url: "pages/pageOne" }) 180 181// replace page 182router.replaceUrl({ url: "pages/pageOne" }) 183 184// clear all page 185this.getUIContext().getRouter().clear() 186 187// 获取页面栈大小 188let size = this.getUIContext().getRouter().getLength() 189 190// 获取页面状态 191let pageState = this.getUIContext().getRouter().getState() 192``` 193 194Navigation通过页面栈对象[NavPathStack](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#navpathstack10)提供的方法来操作页面,需要创建一个栈对象并传入Navigation中。 195 196```ts 197@Entry 198@Component 199struct Index { 200 pathStack: NavPathStack = new NavPathStack() 201 202 build() { 203 // 设置NavPathStack并传入Navigation 204 Navigation(this.pathStack) { 205 // ... 206 }.width('100%').height('100%') 207 .title("Navigation") 208 .mode(NavigationMode.Stack) 209 } 210} 211 212 213// push page 214this.pathStack.pushPath({ name: 'pageOne' }) 215 216// pop page 217this.pathStack.pop() 218this.pathStack.popToIndex(1) 219this.pathStack.popToName('pageOne') 220 221// replace page 222this.pathStack.replacePath({ name: 'pageOne' }) 223 224// clear all page 225this.pathStack.clear() 226 227// 获取页面栈大小 228let size: number = this.pathStack.size() 229 230// 删除栈中name为PageOne的所有页面 231this.pathStack.removeByName("pageOne") 232 233// 删除指定索引的页面 234this.pathStack.removeByIndexes([1, 3, 5]) 235 236// 获取栈中所有页面name集合 237this.pathStack.getAllPathName() 238 239// 获取索引为1的页面参数 240this.pathStack.getParamByIndex(1) 241 242// 获取PageOne页面的参数 243this.pathStack.getParamByName("pageOne") 244 245// 获取PageOne页面的索引集合 246this.pathStack.getIndexByName("pageOne") 247// ... 248``` 249 250Router作为全局通用模块,可以在任意页面中调用,Navigation作为组件,子页面想要做路由需要拿到Navigation持有的页面栈对象NavPathStack,可以通过如下几种方式获取: 251 252**方式一**:通过`@Provide`和`@Consume`传递给子页面(有耦合,不推荐)。 253 254```ts 255// Navigation根容器 256@Entry 257@Component 258struct Index { 259 // Navigation创建一个Provide修饰的NavPathStack 260 @Provide('pathStack') pathStack: NavPathStack = new NavPathStack() 261 262 build() { 263 Navigation(this.pathStack) { 264 // ... 265 } 266 .title("Navigation") 267 .mode(NavigationMode.Stack) 268 } 269} 270 271// Navigation子页面 272@Component 273export struct PageOne { 274 // NavDestination通过Consume获取到 275 @Consume('pathStack') pathStack: NavPathStack; 276 277 build() { 278 NavDestination() { 279 // ... 280 } 281 .title("PageOne") 282 } 283} 284``` 285 286**方式二**:子页面通过`OnReady`回调获取。 287 288```ts 289@Component 290export struct PageOne { 291 pathStack: NavPathStack = new NavPathStack() 292 293 build() { 294 NavDestination() { 295 // ... 296 }.title('PageOne') 297 .onReady((context: NavDestinationContext) => { 298 this.pathStack = context.pathStack 299 }) 300 } 301} 302``` 303 304**方式三**: 通过全局的`AppStorage`接口设置获取。 305 306```ts 307@Entry 308@Component 309struct Index { 310 pathStack: NavPathStack = new NavPathStack() 311 312 // 全局设置一个NavPathStack 313 aboutToAppear(): void { 314 AppStorage.setOrCreate("PathStack", this.pathStack) 315 } 316 317 build() { 318 Navigation(this.pathStack) { 319 // ... 320 }.title("Navigation") 321 .mode(NavigationMode.Stack) 322 } 323} 324 325// Navigation子页面 326@Component 327export struct PageOne { 328 // 子页面中获取全局的NavPathStack 329 pathStack: NavPathStack = AppStorage.get("PathStack") as NavPathStack 330 331 build() { 332 NavDestination() { 333 // ... 334 } 335 .title("PageOne") 336 } 337} 338``` 339 340**方式四**:通过自定义组件查询接口获取,参考[queryNavigationInfo](../reference/apis-arkui/arkui-ts/ts-custom-component-api.md#querynavigationinfo12)。 341 342```ts 343// 子页面中的自定义组件 344@Component 345struct CustomNode { 346 pathStack: NavPathStack = new NavPathStack() 347 348 aboutToAppear() { 349 // query navigation info 350 let navigationInfo: NavigationInfo = this.queryNavigationInfo() as NavigationInfo 351 this.pathStack = navigationInfo.pathStack; 352 } 353 354 build() { 355 Row() { 356 Button('跳转到PageTwo') 357 .onClick(() => { 358 this.pathStack.pushPath({ name: 'pageTwo' }) 359 }) 360 } 361 } 362} 363``` 364 365## 生命周期 366 367Router页面生命周期为`@Entry`页面中的通用方法,主要有如下四个生命周期: 368 369```ts 370// 页面创建后挂树的回调 371aboutToAppear(): void { 372} 373 374// 页面销毁前下树的回调 375aboutToDisappear(): void { 376} 377 378// 页面显示时的回调 379onPageShow(): void { 380} 381 382// 页面隐藏时的回调 383onPageHide(): void { 384} 385``` 386 387其生命周期时序如下图所示: 388 389 390 391Navigation作为路由容器,其生命周期承载在NavDestination组件上,以组件事件的形式开放。 392具体生命周期描述请参考Navigation[页面生命周期](arkts-navigation-navigation.md#页面生命周期)。 393 394```ts 395@Component 396struct PageOne { 397 aboutToDisappear() { 398 } 399 400 aboutToAppear() { 401 } 402 403 build() { 404 NavDestination() { 405 // ... 406 } 407 .onWillAppear(() => { 408 }) 409 .onAppear(() => { 410 }) 411 .onWillShow(() => { 412 }) 413 .onShown(() => { 414 }) 415 .onWillHide(() => { 416 }) 417 .onHidden(() => { 418 }) 419 .onWillDisappear(() => { 420 }) 421 .onDisAppear(() => { 422 }) 423 } 424} 425``` 426 427## 转场动画 428 429Router和Navigation都提供了系统的转场动画也提供了自定义转场的能力。 430 431其中Router自定义页面转场通过通用方法`pageTransition()`实现,具体可参考Router[页面转场动画](arkts-page-transition-animation.md)。 432 433Navigation作为路由容器组件,其内部的页面切换动画本质上属于组件跟组件之间的属性动画,可以通过Navigation中的[customNavContentTransition](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#customnavcontenttransition11)事件提供自定义转场动画的能力,具体实现可以参考Navigation[自定义转场](arkts-navigation-navigation.md#自定义转场)。(注意:Dialog类型的页面当前没有转场动画) 434 435## 共享元素转场 436 437页面和页面之间跳转的时候需要进行共享元素过渡动画,Router可以通过通用属性`sharedTransition`来实现共享元素转场,具体可以参考如下链接: 438[Router共享元素转场动画](../reference/apis-arkui/arkui-ts/ts-transition-animation-shared-elements.md)。 439 440Navigation也提供了共享元素一镜到底的转场能力,需要配合`geometryTransition`属性,在子页面(NavDestination)之间切换时,可以实现共享元素转场,具体可参考[Navigation共享元素转场动画](arkts-navigation-navigation.md#共享元素转场)。 441 442## 跨包路由 443 444Router可以通过命名路由的方式实现跨包跳转。 445 4461. 在想要跳转到的共享包[HAR](../quick-start/har-package.md)或者[HSP](../quick-start/in-app-hsp.md)页面里,给@Entry修饰的自定义组件[EntryOptions](../ui/state-management/arkts-create-custom-components.md#entryoptions10)命名。 447 448 ```ts 449 // library/src/main/ets/pages/Index.ets 450 // library为新建共享包自定义的名字 451 @Entry({ routeName: 'myPage' }) 452 @Component 453 export struct MyComponent { 454 build() { 455 Row() { 456 Column() { 457 Text('Library Page') 458 .fontSize(50) 459 .fontWeight(FontWeight.Bold) 460 } 461 .width('100%') 462 } 463 .height('100%') 464 } 465 } 466 ``` 467 4682. 配置成功后需要在跳转的页面中引入命名路由的页面并跳转。 469 470 ```ts 471 import { router } from '@kit.ArkUI'; 472 import { BusinessError } from '@kit.BasicServicesKit'; 473 import('library/src/main/ets/pages/Index'); // 引入共享包中的命名路由页面 474 475 @Entry 476 @Component 477 struct Index { 478 build() { 479 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 480 Text('Hello World') 481 .fontSize(50) 482 .fontWeight(FontWeight.Bold) 483 .margin({ top: 20 }) 484 .backgroundColor('#ccc') 485 .onClick(() => { // 点击跳转到其他共享包中的页面 486 try { 487 this.getUIContext().getRouter().pushNamedRoute({ 488 name: 'myPage', 489 params: { 490 data1: 'message', 491 data2: { 492 data3: [123, 456, 789] 493 } 494 } 495 }) 496 } catch (err) { 497 let message = (err as BusinessError).message 498 let code = (err as BusinessError).code 499 console.error(`pushNamedRoute failed, code is ${code}, message is ${message}`); 500 } 501 }) 502 } 503 .width('100%') 504 .height('100%') 505 } 506 } 507 ``` 508 509Navigation作为路由组件,默认支持跨包跳转。 510 5111. 从HSP(HAR)中完成自定义组件(需要跳转的目标页面)开发,将自定义组件申明为export。 512 513 ```ts 514 @Component 515 export struct PageInHSP { 516 build() { 517 NavDestination() { 518 // ... 519 } 520 } 521 } 522 ``` 523 5242. 在HSP(HAR)的index.ets中导出组件。 525 526 ```ts 527 export { PageInHSP } from "./src/main/ets/pages/PageInHSP" 528 ``` 529 5303. 配置好HSP(HAR)的项目依赖后,在mainPage中导入自定义组件,并添加到pageMap中,即可正常调用。 531 532 ``` 533 // 1.导入跨包的路由页面 534 import { PageInHSP } from 'library/src/main/ets/pages/PageInHSP' 535 536 @Entry 537 @Component 538 struct mainPage { 539 pageStack: NavPathStack = new NavPathStack() 540 541 @Builder pageMap(name: string) { 542 if (name === 'PageInHSP') { 543 // 2.定义路由映射表 544 PageInHSP() 545 } 546 } 547 548 build() { 549 Navigation(this.pageStack) { 550 Button("Push HSP Page") 551 .onClick(() => { 552 // 3.跳转到Hsp中的页面 553 this.pageStack.pushPath({ name: "PageInHSP" }); 554 }) 555 } 556 .mode(NavigationMode.Stack) 557 .navDestination(this.pageMap) 558 } 559 } 560 ``` 561 562以上是通过**静态依赖**的形式完成了跨包的路由,在大型的项目中一般跨模块的开发需要解耦,那就需要依赖动态路由的能力。 563 564## 动态路由 565 566动态路由设计的目的是解决多个产品(Hap)之间可以复用相同的业务模块,各个业务模块之间解耦(模块之间跳转通过路由表跳转,不需要互相依赖)和路由功能扩展整合。 567 568业务特性模块对外暴露的就是模块内支持完成具体业务场景的多个页面的集合;路由管理就是将每个模块支持的页面都用统一的路由表结构管理起来。 当产品需要某个业务模块时,就会注册对应的模块的路由表。 569 570**动态路由的优势:** 571 5721. 路由定义除了跳转的URL以外,可以丰富的配置任意扩展信息,如横竖屏默认模式,是否需要鉴权等等,做路由跳转时的统一处理。 5732. 给每个路由设置一个名字,按照名称进行跳转而不是ets文件路径。 5743. 页面的加载可以使用动态Import(按需加载),防止首个页面加载大量代码导致卡顿。 575 576**Router实现动态路由主要有下面三个过程:** 577 5781. 定义过程: 路由表定义新增路由 -> 页面文件绑定路由名称(装饰器) -> 加载函数和页面文件绑定(动态import函数)<br> 5792. 定义注册过程: 路由注册(可在入口ability中按需注入依赖模块的路由表)。<br> 5803. 跳转过程: 路由表检查(是否注册过对应路由名称) -> 路由前置钩子(路由页面加载-动态Import) -> 路由跳转 -> 路由后置钩子(公共处理,如打点)。 581 582**Navigation实现动态路由有如下两种实现方案:** 583 584**方案一:** 自定义路由表 585 586基本实现跟上述Router动态路由类似。 5871. 开发者自定义路由管理模块,各个提供路由页面的模块均依赖此模块; 5882. 构建Navigation组件时,将NavPathStack注入路由管理模块,路由管理模块对NavPathStack进行封装,对外提供路由能力; 5893. 各个路由页面不再提供组件,转为提供@build封装的构建函数,并再通过WrappedBuilder封装后,实现全局封装; 5904. 各个路由页面将模块名称、路由名称、WrappedBuilder封装后构建函数注册如路由模块; 5915. 当路由需要跳转到指定路由时,路由模块完成对指定路由模块的动态导入,并完成路由跳转。 592 593具体的构建过程,可以参考Navigation[自动生成动态路由](https://gitee.com/harmonyos-cases/cases/blob/master/CommonAppDevelopment/common/routermodule/README_AUTO_GENERATE.md)示例。 594 595**方案二:** 系统路由表 596 597从API version 12版本开始,Navigation支持系统跨模块的路由表方案,整体设计是将路由表方案下沉到系统中管理,即在需要路由的各个业务模块(HSP/HAR)中独立配置`router_map.json`文件,在触发路由跳转时,应用只需要通过`NavPathStack`进行路由跳转,此时系统会自动完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块解耦。 598具体可参考Navigation[系统路由表](arkts-navigation-navigation.md#系统路由表)。 599 600## 生命周期监听 601 602Router可以通过observer实现注册监听,接口定义请参考Router无感监听[observer.on('routerPageUpdate')](../reference/apis-arkui/js-apis-arkui-observer.md#observeronrouterpageupdate11)。 603 604 605```ts 606import { uiObserver } from '@kit.ArkUI'; 607 608function callBackFunc(info: uiObserver.RouterPageInfo) { 609 console.info("RouterPageInfo is : " + JSON.stringify(info)) 610} 611 612// used in ability context. 613uiObserver.on('routerPageUpdate', this.context, callBackFunc); 614 615// used in UIContext. 616uiObserver.on('routerPageUpdate', this.getUIContext(), callBackFunc); 617``` 618 619在页面状态发生变化时,注册的回调将会触发,开发者可以通过回调中传入的入参拿到页面的相关信息,如:页面的名字,索引,路径,生命周期状态等。 620 621Navigation同样可以通过在observer中实现注册监听。 622 623```ts 624// EntryAbility.ets 625import { BusinessError } from '@kit.BasicServicesKit'; 626import { UIObserver } from '@kit.ArkUI'; 627 628export default class EntryAbility extends UIAbility { 629 // ... 630 onWindowStageCreate(windowStage: window.WindowStage): void { 631 // ... 632 windowStage.getMainWindow((err: BusinessError, data) => { 633 // ... 634 let windowClass = data; 635 // 获取UIContext实例。 636 let uiContext: UIContext = windowClass.getUIContext(); 637 // 获取UIObserver实例。 638 let uiObserver : UIObserver = uiContext.getUIObserver(); 639 // 注册DevNavigation的状态监听. 640 uiObserver.on("navDestinationUpdate",(info) => { 641 // NavDestinationState.ON_SHOWN = 0, NavDestinationState.ON_HIDE = 1 642 if (info.state == 0) { 643 // NavDestination组件显示时操作 644 console.info('page ON_SHOWN:' + info.name.toString()); 645 } 646 }) 647 }) 648 } 649} 650``` 651 652## 页面信息查询 653 654为了实现页面内自定义组件跟页面解耦,自定义组件中提供了全局查询页面信息的接口。 655 656Router可以通过[queryRouterPageInfo](../reference/apis-arkui/arkui-ts/ts-custom-component-api.md#queryrouterpageinfo12)接口查询当前自定义组件所在的Page页面的信息,其返回值包含如下几个属性,其中pageId是页面的唯一标识符: 657 658| 名称 | 类型 | 必填 | 说明 | 659| -------------------- | --------------------------- | ---- | ------------------------------ | 660| context | UIAbilityContext/ UIContext | 是 | routerPage页面对应的上下文信息。 | 661| index | number | 是 | routerPage在栈中的位置。 | 662| name | string | 是 | routerPage页面的名称。 | 663| path | string | 是 | routerPage页面的路径。 | 664| state | RouterPageState | 是 | routerPage页面的状态。 | 665| pageId<sup>12+</sup> | string | 是 | routerPage页面的唯一标识。 | 666 667```ts 668import { uiObserver } from '@kit.ArkUI'; 669 670// 页面内的自定义组件 671@Component 672struct MyComponent { 673 aboutToAppear() { 674 let info: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo(); 675 } 676 677 build() { 678 // ... 679 } 680} 681``` 682 683Navigation也可以通过[queryNavDestinationInfo](../reference/apis-arkui/arkui-ts/ts-custom-component-api.md#querynavdestinationinfo)接口查询当前自定义组件所在的NavDestination的信息,其返回值包含如下几个属性,其中navDestinationId是页面的唯一标识符: 684 685| 名称 | 类型 | 必填 | 说明 | 686| ----------------------------- | ------------------- | ---- | -------------------------------------------- | 687| navigationId | ResourceStr | 是 | 包含NavDestination组件的Navigation组件的id。 | 688| name | ResourceStr | 是 | NavDestination组件的名称。 | 689| state | NavDestinationState | 是 | NavDestination组件的状态。 | 690| index<sup>12+<sup> | number | 是 | NavDestination在页面栈中的索引。 | 691| param<sup>12+<sup> | Object | 否 | NavDestination组件的参数。 | 692| navDestinationId<sup>12+<sup> | string | 是 | NavDestination组件的唯一标识ID。 | 693 694```ts 695import { uiObserver } from '@kit.ArkUI'; 696 697@Component 698export struct NavDestinationExample { 699 build() { 700 NavDestination() { 701 MyComponent() 702 } 703 } 704} 705 706@Component 707struct MyComponent { 708 navDesInfo: uiObserver.NavDestinationInfo | undefined 709 710 aboutToAppear() { 711 this.navDesInfo = this.queryNavDestinationInfo(); 712 console.log('get navDestinationInfo: ' + JSON.stringify(this.navDesInfo)) 713 } 714 715 build() { 716 // ... 717 } 718} 719``` 720 721## 路由拦截 722 723Router原生没有提供路由拦截的能力,开发者需要自行封装路由跳转接口,并在自己封装的接口中做路由拦截的判断并重定向路由。 724 725Navigation提供了[setInterception](../reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md#setinterception12)方法,用于设置Navigation页面跳转拦截回调。具体可以参考文档:Navigation[路由拦截](arkts-navigation-navigation.md#路由拦截)