本文为JS老狗原创。
当前端不得不关注的点:路由,今天聊一聊鸿蒙相关的一点心得。
总体上套路不意外,基本就是(尤其是Web)前端那些事:维护路由表、跳转带参数、历史堆栈操作,等等。
历史原因,ArkTS提供了两套方案:router和Navigation。我厂进入比较早,还是采用的router方案;Navigation的方案只是个人大致研究了一下。下面分别聊一聊。
使用@ohos.router
通过路由地址跳转
当我们以下图的路径创建页面时,开发工具会自动记录一个页面路由:
文件路径:src/main/resources/base/profile/main_pages.json
在同一module中,我们可以使用@ohos.router
库快速实现页面跳转:
import router from '@ohos.router';@Entry@Componentstruct Index { build() { Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Button('Page1').onClick(() => { router.pushUrl({ url: 'pages/Page1' }) }) } .width('100%').height('100%') }}
效果如下:
这个操作过程,跟小程序的创建页面和自动记录路由的过程非常类似,跳转过程跟各路router
也差不多。
当然我们也可手动创建文件,以及手工维护这个路由表。
通过路由命名跳转
我们可以在@Entry
装饰器上,对页面进行路由命名:
@Entry({ routeName: 'Page2' })@Componentstruct Page2 { @State message: string = 'Page2'; build() { RelativeContainer() { Text(this.message) .id('Page2HelloWorld') .fontSize(50) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) } .height('100%') .width('100%') }}
然后在索引页面上,import
这个页面,通知注册路由名称:
import router from '@ohos.router';import './Page2'// ...
这里只是为了注册routeName
,需要代码层import
一个页面,并不是很优雅。
另外还有一种import('./Page2.ets')
的方式,意思差不多,只是这里算动态引用。我们在某些windows模拟器上发现只能使用这种动态引用,复现不稳定,如有问题可试试这种方式。
新增一个按钮,使用router.pushNamedRoute
向Page2
跳转:
Button('Page2').onClick(() => { router.pushNamedRoute({ name: 'Page2' })})
看下效果:
由于路由表是维护在module内的,所以当时项目使用多module时,使用routeName跳转会比较方便。唯一的缺点就是需要额外import页面。
参数传递
跳转时代入params
,用于传递参数:
router.pushUrl({ url: 'pages/Page1', params: { productId: '123' } })
在目标页,使用router.getParams()
获取参数:
import router from '@ohos.router';@Entry@Componentstruct Page1 { @State message: string = 'Page1'; @State productId: string = '' onPageShow(): void { const params = (router.getParams() || {}) as Record<string, Object> this.productId = `${params.productId || ''}` } build() { // ... }}
请注意router.getParams()
又可能返回null
,取值请注意降级。
另外,上面例子是在onPageShow
阶段获取,如果是从其他页面back回来的,这种方式有可能会导致页面参数取值错误。
使用NavPathStack+Navigation
其中NavPathStack
是路由堆栈,Navigation
是UI组件,可以快捷实现页头、底部tabBar等功能能。两者必须结合使用。
构建索引页
我们把Index
页面重构一下:
import router from '@ohos.router';import './Page2'@Entry@Componentstruct Index { routeStack = new NavPathStack() build() { Navigation(this.routeStack) { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Button('Page1').onClick(() => { router.pushUrl({ url: 'pages/Page1' }) }).margin({ bottom: 10 }) Button('Page2').onClick(() => { router.pushNamedRoute({ name: 'Page2' }) }).margin({ bottom: 10 }) } .width('100%').height('100%') } }}
由于我们未对Navigation
组件做任何配置,所以现在页面看不出变化。
维护路由表
这个过程可以简单分3步,请依次完成。首先打开冰箱……
1、创建Page3
,仅用@Component
装饰;使用Page3
构建一个@Builder
声明为Page3Builder
并导出:
@Builderexport function Page3Builder() { Page3()}@Componentstruct Page3 { @State message: string = 'Page3'; build() { RelativeContainer() { Text(this.message)//... } .height('100%') .width('100%') }}
2、在目录src/main/resources/base/profile
中,创建文件route_map.json
,指向上面的Page3
,命名为page3
。
这里的命名建议不要区分大小写,比如驼峰什么的就算了,全小写+数字+下划线不容易出错。
{ "routerMap": [ { "name": "page3", "pageSourceFile": "src/main/ets/pages/Page3.ets", "buildFunction": "Page3Builder" } ]}
3、将路由表在module.json5
中注册:
{ "module": { // ... "routerMap": "$profile:route_map" // ... }}
4、没想到吧,其实这一步最重要了:使用NavDestination
包裹Page3
,否则跳过去也是白屏。
@Builderexport function Page3Builder() { Page3()}@Componentstruct Page3 { @State message: string = 'Page3'; build() { NavDestination() { RelativeContainer() { Text(this.message) .id('Page3HelloWorld') .fontSize(50) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) } .height('100%') .width('100%') } }}
5、对其实还有一步:在索引页中发起跳转:
Button('Page3').onClick(() => { this.routeStack.pushPath({ name: 'page3' })}).margin({ bottom: 10 })
注意这里的跳转名应当严格一致。
看下效果:
当然你一定发现了,Page3
的左上角有个返回按钮,这就是NavDestination
的效果之一。Navigation
相关的路由跳转,现在是官方的推荐做法。
参数传递
哎,令人无奈。两个槽点:
哪里冒出来的unknown
类型;router的参数名是params
,怎么这里又成了param
? 改造Page3
@Builderexport function Page3Builder(name: string, param: Object) { Page3({ param })}@Componentstruct Page3 { @State message: string = 'Page3'; @State product: string = '' param: Object = {} as Record<string, Object> build() { NavDestination() { RelativeContainer() { Text(`${this.message} - ${Reflect.get(this.param, 'product') || ''}`) //... } .height('100%') .width('100%') } }}
请注意Page3Builder
的参数传递,以及在Page3
构建时传入的参数。
另外也可在NavDestination.onReady
周期获取上下文,从上下文中拿到参数:
build() { NavDestination() { // 。。。 } .onReady(context => { this.product = Reflect.get(context.pathInfo.param, 'product') })}
别问Reflect.get
是啥,用就是了。
Navigation的UI配置
Navigation
的UI配置非常丰富,可以看出这个组件在开发时对各路路由UI都做了充分的调研,用起来让人觉得简单得有点不敢相信。
构建页头
最简单的,给页头起个名字,并设置页头大小。
@Entry@Componentstruct Index { routeStack = new NavPathStack() build() { Navigation(this.routeStack) { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { // ... } .width('100%').height('100%') .backgroundColor('#fff') } .title('首页') .titleMode(NavigationTitleMode.Mini) .backgroundColor('#f1f1f1') }}
看下效果:
这里titleMode
的枚举值有:
NavigationTitleMode.Mini
NavigationTitleMode.Full / NavigationTitleMode.Free 自定义UI的页头
在页面中写个@Builder
:
@BuilderNavBar() { Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center, }) { Text('首页').fontSize(16) } .width('100%') .height('100%') .position({ x: 0 }) .zIndex(-1)}
在build()
中输入这个自定义页头:
build() { Navigation(this.routeStack) { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Button('Page1').onClick(() => { router.pushUrl({ url: 'pages/Page1' }) }).margin({ bottom: 10 }) Button('Page2').onClick(() => { router.pushNamedRoute({ name: 'Page2' }) }).margin({ bottom: 10 }) Button('Page3').onClick(() => { this.routeStack.pushPath({ name: 'page3' }) }).margin({ bottom: 10 }) } .width('100%').height('100%') .backgroundColor('#fff') } .title(this.NavBar) // <<<<------- 看这里 .titleMode(NavigationTitleMode.Mini) .backgroundColor('#f1f1f1')}
请注意NavBar
中用了一个zIndex(-1)
,这样就不会遮挡返回按钮了。
底部TabBar配置
先看下效果:
toolbarConfiguration
build() { Navigation(this.routeStack) { // ... } .title(this.NavBar) .titleMode(NavigationTitleMode.Mini) .backgroundColor('#f1f1f1') .toolbarConfiguration([ { icon: 'https://res.suning.cn/project/cmsWeb/suning/homepage/v8/css/images/tool-logo.png', value: '首页', action: () => { router.pushUrl({ url: 'pages/Page1' }) } }, { icon: 'https://image.suning.cn/uimg/cms/img/157105762930982264.png', value: '购物车', action: () => { this.routeStack.pushPath({ name: 'page3' }) } }, { icon: 'https://image.suning.cn/uimg/cms/img/157105768303272975.png', value: '我的', action: () => { router.pushNamedRoute({ name: 'Page2' }) } } ])}
简简单单,配置icon/value/action,即出成品。
写在最后
这个阶段对路由的研究,大致就是如此。感觉像Navigation
在UI方面的表现,应该还有不少可以深挖的地方。
前面也有大佬们提过,ArkTS跟Flutter像,以及模式、架构跟mvvm也有相近之处,到了路由这部分,其实跟rn也有些相似了。
作为后来者,相信ArkTS能够吸取众家之长,成为集大成者。
关于OpenTiny
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:https://opentiny.design/
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
OpenTiny HUICharts 源码:https://github.com/opentiny/tiny-charts
欢迎进入代码仓库 Star?TinyEngine、TinyVue、TinyNG、TinyCLI~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~
(温馨提示:OpenTiny CCF开源创新大赛也在持续报名中,欢迎大家一起报名参赛,赢取10W奖金)