• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![image](figures/router_page_lifecycle.png)
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#路由拦截)