鸿蒙应用开辟从入门到入魔: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标准库中的某些接口。大部分接口与动态特性有关。
复制代码 从TS到ArkTS的适配规则
动态返回实例
既然不让反射创建实例,那我们直接创建好实例,然后和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页面的管理都不会这么复杂[捂脸]。
只能盼望官方能尽快优化一下利用方式吧。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |