文章目录
- 应用导航设计
- 引言
- 概述
- 场景示例
- 基本实现
- 推荐方案
- 路由管理模块的实现
- 页面跳转实现
- 业务实现中的关键点
- 动态加载
- 路由栈管理
应用导航设计
引言
在大型应用开发中,如何高效地设计应用导航,处理多模块间的路由跳转与解耦,始终是一个关键挑战。随着应用规模的扩大,业务模块增多,各模块间的 UI 组件相互跳转需求频繁出现,传统的导航设计往往导致模块间相互依赖、耦合严重,影响开发效率、可维护性以及应用的启动速度。本文将深入探讨应用导航设计的优化方案,从华为官方提供的基于 Navigation 的路由设计基础出发,剖析其存在的问题,并详细阐述推荐的路由功能抽取与模块解耦方案,帮助开发者更好地应对大型应用导航设计中的复杂问题,提升应用开发的整体质量与效率。
概述
大型应用开发中,应用可能包含不同的业务模块,每个模块由不同的业务团队负责开发。该场景采用一个Navigation下多个har/hsp的架构,其中一个模块对应一个har/hsp。当多个har/hsp的UI组件存在相互跳转的业务需求时,将出现模块间相互依赖的问题。如“A.har”、“B.har”和“C.har”模块拥有不同的组件,各组件间的路由跳转形成了一个环形链路,导致三个har模块相互耦合,如图所示:
针对该场景,华为官方也提供了一套基于Navigation的路由设计的方案实现多模块路由管理和模块间解耦。并在该基础上,通过动态注册路由的方式,解决页面加载多个UI组件时启动速度变慢问题。Navigation组件的使用方法可以见
在哪里?在哪里!设置到底在哪里!!!HarmonyOS移动应用开发——设置组件导航(Navigation组件、Tabs组件实现页面导航)
场景示例
假定工程包含harA和harB两个业务模块,harA模块打包编译为A.har,harB模块打包编译为B.har。在实际业务中,harA模块中的A1组件需要跳转到harB模块中的B1组件,项目关系如下图所示。例如登录场景,登录界面和登录后的主要页面是两个独立的模块,用户在登录结束之后,就可以直接进入首页开始使用app相关内容。
基本实现
假设我们现在已经拥有了har.A:default和A1:MaiPage,以及har.B:homepage和B1:HomePageView,和har.C:mine和C1:MineView
具体实现步骤如下
-
在defaule模块中的MaiPage页面组件中开发Navigation组件,并关联与之对应的NavPathStack路由栈,示例代码如下:
@Component struct MaiPage {// 创建NavPathStack路由栈对象@Provide('MainPagePathStack') MainPagePathStack: NavPathStack = new NavPathStack();build() {// Navigation组件关联NavPathStack对象Navigation(this.MainPagePathStack) {// ...}} }
-
在defaule模块的oh-package.json5文件中添加Mine模块和homepage模块的依赖,示例代码如下:
"dependencies": {// 添加依赖"homepage": "file:../../features/homepage","mine": "file:../../features/mine" }
-
在defaulet模块的mainpage中借助Tabs组件实现页面的跳转的逻辑,完整实例代码如下:
//default模块的mainpage组件 import { HomePageView } from 'homepage' import { MineView } from 'mine'; import { TabBarType } from '../model/TabBarModel'; import { CustomTabBar } from '../components/CustomTabBar';@Entry @Component struct MainPage {// 创建NavPathStack路由栈@Provide('MainPagePathStack') MainPagePathStack: NavPathStack = new NavPathStack();@Provide('minePathStack') minePathStack: NavPathStack = new NavPathStack();@Provide('homePagePathStack') homePagePathStack: NavPathStack = new NavPathStack();@State currentIndex: TabBarType = TabBarType.MAINPAGEbuild() {// Navigation关联NavPathStack对象Navigation(this.MainPagePathStack) {Flex({ direction: FlexDirection.Column }) {Tabs({ index: this.currentIndex }) {TabContent() {//通过Tabs组件跳转页面,同时在该页面也应该注册HomePageView()}TabContent() {MineView()}}.layoutWeight(1).barHeight(0).scrollable(false).onChange((index) => {this.currentIndex = index;})CustomTabBar({ currentIndex: $currentIndex })}.width('100%')}.hideTitleBar(true).mode(NavigationMode.Stack)} }
//homepage模块的HomePageView组件 @Component export struct HomePageView { //接收相关路由@Consume('homePagePathStack') homePagePathStack: NavPathStack;build() {Navigation(this.homePagePathStack) {Row() {Text('这里是首页')}.width('100%').height('100%')}} }
//mine模块的MineView组件 import { UserInfoView } from "./UserInfoView"; import { MineHeaderView } from "./MineHeaderView";@Component export struct MineView { //接收相关路由@Consume('minePathStack') minePathStack: NavPathStack;build() {Navigation(this.minePathStack) {Row() {Column() {MineHeaderView().margin({ top: 10 })UserInfoView().margin({ top: 20 })}.justifyContent(FlexAlign.Start).alignItems(HorizontalAlign.Center).width('100%')}.alignItems(VerticalAlign.Top).width('100%').height('100%')}} }
当前方案主要存在以下问题:
- 模块无法独立编译且存在开发态模块间循环依赖问题。
- 子模块的页面跳转也可能出现过于耦合的情况。
- 因为使用了Tabs组件,所以跳转的组件是平行跳转的,而在真正的项目开发中,在TabContent中也一定会再次跳转。
所以官方还给出了routerMap路由表的方式:
-
在harA模块中的A1页面组件中开发Navigation组件,并关联与之对应的NavPathStack路由栈,示例代码如下:
@Component struct A1 {// 创建NavPathStack路由栈对象@State harARouter: NavPathStack = new NavPathStack();build() {// Navigation组件关联NavPathStack对象Navigation(this.harARouter) {// ...}} }
-
在harA模块的oh-package.json5文件中添加harB模块的依赖,并且把harB模块中需要跳转的B1组件添加到harA模块的Navigation组件路由表中,示例代码如下
"dependencies": {// 添加对harB的依赖"@ohos/harb": "file:../harB" }
在harA模块的A1组件中的routerMap路由表中,添加harB模块的B1组件,示例代码如下:
import { B1 } from '@ohos/harb'; struct A1 {@State harARouter: NavPathStack = new NavPathStack();@BuilderrouterMap(builderName: string, param: object) {if (builderName === 'B1') {B1() // 在routerMap中添加需要跳转的harB模块的B1页面}}build() {Navigation(this.harARouter) {// ...}.navDestination(this.routerMap) // Navigation关联上routerMap路由表} }
-
在harA模块的Navigation组件中添加跳转到harB模块的B1页面的逻辑,完整示例代码如下:
import { B1 } from '@ohos/harb';struct A1 {// 创建NavPathStack路由栈@State harARouter: NavPathStack = new NavPathStack();@BuilderrouterMap(builderName: string, param: object) {if (builderName === 'B1') {B1() // 在routerMap中添加需要跳转的harB模块的B1页面}}build() {// Navigation关联NavPathStack对象Navigation(this.harARouter) {Button('跳转到HarB的B1页面').onClick(() => {// 跳转到已在路由表注册的harB模块的B1页面this.harARouter.pushPathByName('B1', null);})}.navDestination(this.routerMap) // Navigation关联上routerMap路由表} }
当前基本方案主要存在以下问题:
- 使用Navigation时,所有路由页面需要主动通过import方式逐个导入当前页面,并存入页面路由表routerMap中。
- 主动使用import的方式需显性指定加载路径,造成开发态模块耦合严重。
- 模块无法独立编译,且存在开发态模块间循环依赖问题。
为解决上述问题,推荐如下方案。
推荐方案
将路由功能抽取成单独的模块并以har包形式存在,命名为RouterModule。RouterModule内部对路由进行管理,对外暴露RouterModule对象供其他模块使用。由于Entry.hap是应用必备的主入口,利用该特性考虑将主入口模块作为其他业务模块的依赖注册中心,在入口模块中使用Navigation组件并依赖其他业务模块。业务模块仅依赖RouterModule,业务模块中的路由统一委托到RouterModule中管理,实现业务模块间的解耦。按照推荐方案,上述场景各模块依赖关系如下:
此方案中,各模块的依赖关系如下:
- Entry.hap、A.har和B.har均依赖了RouterModule.har;
- Entry.hap在工程配置中依赖了A.har和B.har;
- 对于业务开发团队之间,A.har在工程和源码上无需依赖B.har的库,实现了业务模块间的解耦。
路由管理模块的实现
RouterModule模块包含全局的路由栈和路由表信息。路由栈是NavPathStack对象,该对象与Entry.hap的Navigation组件绑定,RouterModule通过持有NavPathStack管理Navigation组件的路由信息。路由表builderMap是Map结构,以key-vaule的形式存储了需要路由的页面组件信息,其中key是自定义的唯一路由名,value是WrappedBuilder对象,该对象包裹了路由名对应的页面组件。RouterModule模块结构如下:
RouterModule模块的实现主要包含以下步骤:
- 定义路由表和路由栈。
export class RouterModule {// WrappedBuilder支持@Builder描述的组件以参数的形式进行封装存储static builderMap: Map<string, WrappedBuilder<[object]>> = new Map<string, WrappedBuilder<[object]>>();// 初始化路由栈,需要关联Navigation组件static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>();// ...
}
- 路由表增加路由注册和路由获取方法,业务har模块通过路由注册方法将需要路由的页面组件委托给RouterModule管理。
// 通过名称注册路由栈
public static registerBuilder(builderName: string, builder: WrappedBuilder<[object]>): void {RouterModule.builderMap.set(builderName, builder);
}// 获取路由表中指定的页面组件
public static getBuilder(builderName: string): WrappedBuilder<[object]> {const builder = RouterModule.builderMap.get(builderName);if (!builder) {Logger.info('not found builder ' + builderName);}return builder as WrappedBuilder<[object]>;
}
3.路由表增加路由跳转方法,业务har模块通过调用该方法并指定跳转信息实现模块间路由跳转。
public static async push(router: RouterModel): Promise<void> {const harName = router.builderName.split('_')[0];await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName));RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param });
}
页面跳转实现
路由管理模块RouterModule实现之后,需要使用RouterModule模块实现业务模块harA的页面跳转到业务模块harB的页面功能。主要步骤如下:
-
在工程主入口模块Entry.hap中引入RouterModule模块和所有需要进行路由注册的业务har模块。
"dependencies": {"@ohos/routermodule": "file:../RouterModule","@ohos/hara": "file:../harA","@ohos/harb": "file:../harB","@ohos/harc": "file:../harC" }
-
在工程主入口模块Entry.hap中配置build-profile.json5文件,在该文件中修改packages字段,将需要进行路由注册的业务har模块写入配置。
{// ..."buildOption": {"arkOptions": {"runtimeOnly": {"sources": [],"packages": ["@ohos/hara","@ohos/harb","@ohos/harc"]}}}, }
-
在工程主入口模块的首页Navigation组件上关联RouterModule模块的路由栈和路由表。其中RouterModule.createRouter()与RouterModule.getBuilder()方法的实现。
@Entry @Component struct EntryHap {@State entryHapRouter: NavPathStack = new NavPathStack();aboutToAppear() {if (!this.entryHapRouter) {this.entryHapRouter = new NavPathStack();}RouterModule.createRouter(RouterNameConstants.ENTRY_HAP, this.entryHapRouter);};@BuilderrouterMap(builderName: string, param: object) {// Obtain the WrappedBuilder object based on the module name, create a page through the builder interface, and import the param parameter.RouterModule.getBuilder(builderName).builder(param);};build() {Navigation(this.entryHapRouter) {// ...}.title('NavIndex').navDestination(this.routerMap);} }
-
在harB中声明需要跳转的页面,并且调用registerBuilder接口将页面注册到RouterModule模块的全局路由表上。以下注册逻辑会在harB的B1页面被首次加载时触发。
// harB模块的B1页面 @Builder export function harBuilder(value: object) {NavDestination() {Column() {// ...}// ...}// ... }// 在页面首次加载时触发执行 const builderName = BuilderNameConstants.HARB_B1; // 判断表中是否已存在路由信息,避免重复注册 if (!RouterModule.getBuilder(builderName)) {// 通过系统提供的wrapBuilder接口封装@Builder装饰的方法,生成harB1页面builderlet builder: WrappedBuilder<[object]> = wrapBuilder(harBuilder);// 注册harB1页面到全局路由表RouterModule.registerBuilder(builderName, builder); }
-
在harA模块中的A1页面调用RouterModule模块的push方法实现跳转到harB的B1页面。当harB的B1页面被首次通过push方法跳转时,会动态加载B1页面,并且触发步骤4中B1页面的路由注册逻辑,把B1页面注册到RouterModule的全局路由表builderMap中。
@Builder export function harBuilder(value: object) {NavDestination() {Column() {// ...Button($r("app.string.to_harb_pageB1"), { stateEffect: true, type: ButtonType.Capsule }).width('80%').height(40).margin(20).onClick(() => {buildRouterModel(RouterNameConstants.ENTRY_HAP, BuilderNameConstants.HARB_B1);})}.width('100%').height('100%')}.title('A1Page').onBackPressed(() => {RouterModule.pop(RouterNameConstants.ENTRY_HAP);return true;}) }
上述方案,当在entry模块页面上点击跳转到harA模块的页面时序图如下:
业务实现中的关键点
动态加载
上述方案实现解耦的关键是使用了动态加载的方式和自执行的函数。针对该解决方案可以根据业务需求进一步优化和封装。例如harB中需要注册多个页面:B1、B2、B3。改进方式如下:
- 在harB的对外导出类Index.ets中定义加载时的初始化函数harInit,该函数对harB中需要注册路由的页面组件进行加载管理,被调用时将根据不同的路径动态加载不同的页面。
export function harInit(builderName: string): void {// 根据routerModule中路由表的key值动态加载要跳转的页面的相对路径switch (builderName) {case BuilderNameConstants.HARB_B1:import("./src/main/ets/components/mainpage/B1");break;case BuilderNameConstants.HARB_B2:import("./src/main/ets/components/mainpage/B2");break;case BuilderNameConstants.HARB_B3:import("./src/main/ets/components/mainpage/B3");break;default:break;}
}
- 优化RouterModule模块中的push方法。为了便于路由跳转时能携带更多信息,增加路由信息类RouterModel作为push时的入参。在push方法中通过该参数获取跳转页面所在的包名、路由名和所需的参数信息。通过包名成功加载har 模块后,根据路由名builderName调用har 模块index页面上定义的harInit函数,实现har模块内多页面的动态加载。
// 路由信息类,便于跳转时传递更多信息
export class RouterModel {// 路由页面别名,形式为${包名}_${页面名}builderName: string = "";// 路由栈名称routerName: string = "";// 需要传入页面的参数param?: object = new Object();
}// 创建路由信息,并放到路由栈表中
export function buildRouterModel(routerName: string, builderName: string, param?: object) {let router: RouterModel = new RouterModel();router.builderName = builderName;router.routerName = routerName;router.param = param;RouterModule.push(router);
}
路由栈管理
有些业务场景中存在需要使用多个Navigation组件的情况,该场景下需要在RouterModule中管理多个与Navigation组件对应的路由栈NavPathStack对象。此时,可以在RouterModule模块中建立一个路由栈表,以key-value的形式存储多个Navigation组件对应的的路由栈,以此实现多路由栈的管理。增加路由栈后,RouterModule中的路由方法都需要先通过routerName获取到路由栈,再进行方法调用。代码如下:
export class RouterModule {// ...// 初始化路由栈,需要关联Navigation组件static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>();// 通过名称注册路由栈public static createRouter(routerName: string, router: NavPathStack): void {RouterModule.routerMap.set(routerName, router);}// 通过名称获取路由栈public static getRouter(routerName: string): NavPathStack {return RouterModule.routerMap.get(routerName) as NavPathStack;}// 通过传入RouterModule跳转到指定页面组件,RouterModule中需要增加routerName字段用于获取路由栈public static async push(router: RouterModel): Promise<void> {const harName = router.builderName.split('_')[0];await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName));RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param });}