
鸿蒙技术分享:鸿蒙应用开发从入门到入魔:Navigation路由管理为什么这么麻烦?
鸿蒙应用开发从入门到入魔:Navigation路由管理为什么这么麻烦?
鸿蒙应用的路由管理
鸿蒙应用开发,ArkUI框架提供了两种路由管理方式,分别是@ohos.router(以下简称router)和Navigation。
router管理页面
我们先来看router是怎么管理页面的。
router的页面通过@Entry来声明,支持普通路由和命名路由。
// 定义页面
@Entry
@Component
struct SecondPage {
build() {
Column() {
Text('这是SecondPage')
}
.height('100%')
.width('100%')
}
}
// 打开
router.pushUrl({ url: 'pages/SecondPage' })
// 定义命名路由页面
@Entry({ routeName: 'libraryHar/mainPage' })
@Component
export struct MainPage {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
// 打开
import 'libraryhar/src/main/ets/components/MainPage';
router.pushNamedRoute({ name: 'libraryHar/mainPage' })
虽然router的普通路由和命名路由的区分让项目开发产生了一些额外的复杂度,但router的页面定义方式还是比较符合常用习惯的。
Navigation管理页面
再让我们来看看Navigation是怎么管理页面的。
@Entry
@Component
struct NavigationIndex {
navPathStack: NavPathStack = new NavPathStack()
@Builder
routerMap(name: string, params: ESObject) {
if ('SecondNavDestination' === name) {
SecondNavDestination()
} else {
Text('404 Not Found')
}
}
build() {
Navigation(this.navPathStack) {
Column() {
Text('打开SecondNavDestination')
.onClick(() => {
this.navPathStack.pushDestination({ name: 'SecondNavDestination' })
})
}
}
.navDestination(this.routerMap)
}
}
Navigation是导航容器,具体的页面其实是NavDestination。
Navigation提供了navDestination
方法用来动态创建NavDestination页面。
虽然Navigation的页面管理提供了动态化的能力,但目前的这种写法无法做到解耦,每新增一个页面都要增加一个if语句。
这种方式和router的管理方式相比差别很大,无法满足开发者对于解耦或者动态化的要求。
如何动态化
通过上一节的例子我们可以看到,Navigation标准的路由管理是用builder方法来实现的,如果想要更好的使用Navigation路由,我们首先想到的就是把NavDestination的实例化过程拆分出去,不直接在builder方法中进行。
装饰器?
router是通过@Entry装饰器来声明页面和路由的,Navigation并没有提供类似的装饰器。
那么我们是否可以提供一个类似的装饰器完成相关功能呢?
比如:
interface NavigationEntryOptions {
routeName: string
}
function NavigationEntry(options: NavigationEntryOptions) {
return Object
}
@NavigationEntry({ routeName: "testPage" });
export struct TestPage {
build() {
}
}
但是实际上不行,在ArkTS中,struct不支持添加自定义装饰器,虽然编译器不会报错,但是装饰器却不执行。
反射创建实例?
装饰器不可以,我们然后想到的方法自然是反射,既然要用routeName获取一个Component对象。
但是可惜,ArkTS不允许使用TS的动态化方法:
限制使用标准库
规则:arkts-limited-stdlib
级别:错误
ArkTS不允许使用TypeScript或JavaScript标准库中的某些接口。大部分接口与动态特性有关。
动态返回实例
既然不让反射创建实例,那我们直接创建好实例,然后和routeName设置好匹配关系,然后动态返回呢?
struct WrappedNavigationIndex {
navDestinationMap: Record<string, ESObject> = {}
aboutToAppear(): void {
this.navDestinationMap['WrappedSecondNavDestination'] = WrappedSecondNavDestination()
}
@Builder
routerMap(name: string, params: ESObject) {
this.navDestinationMap[name]
}
}
还是不行,Navigation.navDestination
方法对于返回值类型有要求,必须是@Component装饰的实例。
'PageManager.shared' does not comply with the UI component syntax. <ArkTSCheck>
封装
因为ArkUI的页面是基于struct而不是class,所以无法继承,也无法声明一个符合comply with the UI component syntax
要求的类型。
看似陷入了死胡同,不过官方也考虑到了,所以提供了一个特殊类型:WrappedBuilder
。
可以使用wrapBuilder
函数创建该类型,但需要传入一个@builder
装饰的函数作为参数。
@Entry
@Component
struct WrappedNavigationIndex {
navDestinationMap: Record<string, WrappedBuilder<[params?: ESObject]>> = {}
aboutToAppear(): void {
this.navDestinationMap['WrappedSecondNavDestination'] = wrapBuilder(WrappedSecondNavDestinationBuilder)
}
@Builder
routerMap(name: string, params: ESObject) {
this.navDestinationMap[name].builder()
}
}
@Builder
export function WrappedSecondNavDestinationBuilder() {
WrappedSecondNavDestination()
}
@Component
export struct WrappedSecondNavDestination {
build() {
NavDestination() {
Column() {
Text('这是WrappedSecondNavDestination')
}
}
}
}
如何自动化注册?
通过封装builder,我们做到了动态化返回NavDestination,但NavDestination实例我们目前需要手动一个个创建并注册。
那么有没有办法不需要手动注册,而是像@Entry那样自动创建并注册吗?
注册表:官方方案
我们先来看看官方提供的方案。
方案详情请查看官方文档:系统路由表
{
"routerMap": [
{
"name": "PageOne",
"pageSourceFile": "src/main/ets/pages/PageOne.ets",
"buildFunction": "PageOneBuilder",
"data": {
"description" : "this is PageOne"
}
}
]
}
我们可以看到在路由表配置文件中,有一个字段是buildFunction
,字段说明为跳转目标页的入口函数名称,必须以@Builder修饰。
。
这即是我们文章上面定义的页面builder方法。
系统路由表通过自动解析路由表配置,将页面name
和buildFunction
包装成的WrappedBuilder
实例关联在一起,从而做到自动化注册效果。
代码生成+注册表:鸿蒙案例库方案
该方案是指cases开源案例库中的dynamicRouter。
其原理见自动生成动态路由。
最为核心的功能在于添加装饰器和插件配置文件,编译时自动生成动态路由表
,其通过自定义装饰器和hvigor插件,自动生成builder方法和路由表。
其中的插件就是指cases库中的AutoBuildRouter。
我们来看一下dynamicRouter生成的代码和路由表是什么样的。
// auto-generated
import { DynamicsRouter, AppRouterInfo } from '@ohos/dynamicsrouter/Index';
import { AddressExchangeView } from '../view/AddressExchangeView'
@Builder
function addressExchangeViewBuilder() {
AddressExchangeView();
}
export function addressExchangeViewRegister(routerInfo: AppRouterInfo) {
DynamicsRouter.registerAppRouterPage(routerInfo, wrapBuilder(addressExchangeViewBuilder));
}
{
"routerMap": [
{
"name": "addressexchange/AddressExchangeView",
"pageModule": "addressexchange",
"pageSourceFile": "src/main/ets/generated/RouterBuilder.ets",
"registerFunction": "addressExchangeViewRegister"
}
]
}
这个路由表和系统路由表的最大差别就是没有了buildFunction
字段,而多了registerFunction
字段。
它是做什么用的呢?
import(`${DynamicsRouter.config.libPrefix}/${routerInfo.pageModule}`)
.then((module: ESObject) => {
// 通过路由注册方法注册路由
module[routerInfo.registerFunction!](routerInfo);
// TODO:知识点:在路由模块的动态路由.pushUri()中调用拦截方法,此处必须等待动态路由加载完成后再进行拦截,否则页面加载不成功,导致无法注册拦截的函数,出现首次拦截失效。
if (Interceptor.interceptor(name, param)) {
return;
}
// 跳转页面
DynamicsRouter.navPathStack.pushPath({ name: name, param: param });
})
.catch((error: BusinessError) => {
logger.error(`promise import module failed, error code:${error.code}, message:${error.message}`);
});
在模块按需加载成功后,会调用module对象中的registerFunction方法完成页面路由注册。
可以看到,案例库方案相对于系统方案少了编写builder方法和配置路由表的步骤,将这些操作交给hvigor插件去完成。
代码生成+装饰器:@fw/router方案
无论是官方方案和案例库方案,都要依赖路由表。那么到底有没有办法不依赖路由表呢?
有的。
我们上面说到,struct可以添加自定义装饰器,但自定义装饰器不会触发执行;但是class的装饰器却是可以触发执行的。
所以,可以通过class装饰器完成路由的自动注册。而这就是https://gitee.com/FantasyWind/fwrouter@fw/router的实现方案。
因为struct装饰器不能自动执行,所以@fw/router也是借用hvigor插件扫描struct装饰器并生成模板代码。
我们来看一下它生成的页面路由代码。
// auto-generated
import { RouterClassProvider } from '@fw/router/Index';
import { TestDestination } from '../pages/TestPage'
@Builder
function testDestinationBuilder(params: ESObject) {
TestDestination({ params: params });
}
@RouterClassProvider({ routeName: 'testPage', builder: wrapBuilder(testDestinationBuilder) })
export class TestDestinationProvider {
}
我们又看到了熟悉的builder方法,不过和案例库方案不同的是,这里多了TestDestinationProvider
类,而它有个装饰器@RouterClassProvider
。
@RouterClassProvider
实现了什么功能呢?
export function RouterClassProvider(options: RouterClassProviderOptions) {
return (target: ESObject) => {
RouterManagerForNavigation.getInstance().registerBuilder(options.routeName, options.builder)
}
}
该装饰器在执行时完成了页面路由的注册。更巧的是这个装饰器在模块被动态import时就会触发执行,因此我们既不需要路由表,也不需要在import之后手动调用注册函数。
不过该方案也存在一些缺点,比如目前(api12),类装饰器在hsp包中无法触发执行,因此只能在hap和har中使用。
总结
看完整篇文章我们看到,Navigation路由管理方案之所以变成现在的样子,实在是开发语言存在的诸多限制所导致。
- struct不支持自定义装饰器;
- Navigation跳转不支持直接动态实例化@Component;(因为@Component struct没有类型,只能是ESObject,Navigation.navDesination不支持ESObject)所以只能是用wrapBuilder包装@Builder;
- 不支持反射,动态实例化,@Builder不能动态创建组件,一个Component必须对应一个@Builder;
哪怕官方能小小地放开一个口子,Navigation页面的管理都不会这么复杂[捂脸]。
只能希望官方能尽快优化一下使用方式吧。
更多推荐
所有评论(0)