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