整体功能图
分层架构主要依托于路由解耦,因此路由是组件化或模块化的重中之重,本文重点探讨路由以及路由中的问题,以及官方文档缺少的模糊不清的问题详解
demo地址请看原文
路由的基础使用
路由安装 & 工程配置
- 【新建工程】创建工程,例如工程名称为: myhmrouter
- 【安装】在工程根目录下(壳工程下)安装hmrouter组件依赖,执行命令:
ohpm install @hadss/hmrouter
- 【配置】修改工程的
hvigor/hvigor-config.json
文件,加入路由编译插件 -
{ "dependencies": { "@hadss/hmrouter-plugin": "^1.0.0-rc.10" }, }
- 【配置】修改entry模块的hvigorfile.ts , 路径:entry/hvigorfile.ts
-
import { hapTasks } from '@ohos/hvigor-ohos-plugin'; import { hapPlugin } from '@hadss/hmrouter-plugin'; export default { system: hapTasks, plugins: [hapPlugin()] // 使用HMRouter标签的模块均需要配置,与模块类型保持一致 }
- 【初始化】在UIAbility或者启动框架AppStartup中初始化路由框架
-
export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { HMRouterMgr.init({ context: this.context }) } }
- 【路由入口】HMRouter依赖系统Navigation能力, 所以必须在页面中定义一个
HMNavigation
容器 - 在entry模块Index.ets中定义HMNavigation容器
-
class NavModifier extends AttributeUpdater<NavigationAttribute> { initializeModifier(instance: NavigationAttribute): void { instance.mode(NavigationMode.Stack); // instance.hideNavBar(true) //这句会导致底部导航不展示 instance.navBarWidth('100%') instance.hideTitleBar(true) instance.hideToolBar(true) } } @Preview({ deviceType: 'phone' }) @Entry @Component struct Index { @State currentIndex: number = 0 @Builder tabBuilder(title: string, targetIndex: number, selectedImg: Resource, normalImg: Resource) { Column() { Image(this.currentIndex === targetIndex ? selectedImg : normalImg) .size({ width: 25, height: 25 }) .padding(3) Text(title) .fontColor(this.currentIndex === targetIndex ? '#232323' : '#8e8e8e') .fontSize(10) .margin({ top: 5 }) } .width('100%') .height(50) .justifyContent(FlexAlign.Center) } modifier: NavModifier = new NavModifier(); build() { Column() { HMNavigation({ navigationId: "AppNavigationID", options: { // 设置动画样式 standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR, // 设置弹框动画样式 dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR, // 设置页面navigation的参数,标题栏,工具栏,bar那些 modifier: this.modifier } }) { Tabs({ barPosition: BarPosition.End }) { TabContent() { Text("首页 page") } .tabBar(this.tabBuilder('首页', 0, $r('app.media.icon_mubiao_filled'), $r('app.media.icon_mubiao'))) TabContent() { Text("商城 page") } .tabBar(this.tabBuilder('商城', 1, $r('app.media.icon_daiban_filled_black86'), $r('app.media.icon_daiban_black86'))) TabContent() { Text("购物 page") } .tabBar(this.tabBuilder('购物', 2, $r('app.media.icon_kehu_filled'), $r('app.media.icon_kehu'))) TabContent() { Text("活动 page") } .tabBar(this.tabBuilder('活动', 3, $r('app.media.icon_koubei_filled'), $r('app.media.icon_koubei'))) TabContent() { Text("我的 page") } .tabBar(this.tabBuilder('我的', 4, $r('app.media.icon_wode24_filled_black86'), $r('app.media.icon_wode24_black86'))) } .onChange((index: number) => { this.currentIndex = index }) .barMode(BarMode.Fixed) .scrollable(false) .animationMode(AnimationMode.NO_ANIMATION) } } .height('100%') .width('100%') } }
至此,路由已配置完成 预览器看不到效果 使用模拟器或者真机看效果
路由开发规范
- 除了entry模块外,所有页面都应该定义在ets/components文件夹下
- 拦截器应该定义在ets/interceptors文件夹下
- 自定义动画应定义在ets/animations文件夹下
- 退出登录返回登录页面是否要清栈,可以使用HMRouterMgr.getPathStack(navigationId)?.clear(),然后跳转登录页面
路由协议
路由协议应该和现有的安卓或iOS的路由定义协议保持一致,形如:hhy://user/getImageCode
标准格式就是scheme名称://模块/具体功能
路由跳转
使用HMRouterMgr的push方法进行跳转到目标页
HMRouterMgr.push({
navigationId?: string; // 指定导航HMNavigation的navigationId
pageUrl?: string; // push到哪个页面
param?: ESObject; // 传参对象
interceptors?: IHMInterceptor[]; // 指定路由拦截器
animator?: IHMAnimator | boolean; // 指定跳转动画
skipAllInterceptor?: boolean; // 是否跳过路由拦截
})
路由传参
跳转时使用param传参数 ,例如:
HMRouterMgr.push({
pageUrl: "hhy://LoginPage",
param: { targetUrl: info.targetName }, // 传递的参数可以为任意对象,字符串等基础类型、字典数组等,以及自定义类型对象
skipAllInterceptor: true
})
目标页接收参数
@State param: HMPageParam = HMRouterMgr.getCurrentParam(HMParamType.all) as HMPageParam
路由回调
可以监听路由跳转和返回的回调事件
路由提供的基础监听能力
Button("监听跳转回调").onClick(e => {
HMRouterMgr.push({
pageUrl: "hhy://searchpage/abc",
param: { msg: "监听跳转回调" },
skipAllInterceptor: true
}, {
onResult(popInfo: HMPopInfo) { // 路由跳转后页面返回时调用
console.info("The callback when page returns." + JSON.stringify(popInfo))
},
onArrival() { // 路由成功跳转后调用
console.info("The callback when system push executed successfully.")
},
onLost() { // 路由跳转失败后调用
console.info("The callback when page could not be found.")
}
})
}).width('90%')
路由跳转返回时如何携带参数给上一个页面
路由pop方法可以携带参数给onResult,参数会放到popInfo中,参数可以为任意类型。
HMRouterMgr.pop({
param:{"callbackdata":"this is callback data"} //这里的callbackdata会返回到上一个页面回调onResult中
})
特殊情况(如果用户是使用侧滑手势返回,则无法调用pop方法,此时路由无法携带参数给上一个页面。但是可以 通过传方法类型的参数来自定义实现)
// 定义回调方法
type RouterCallback <T> = (x: T) => void
// 跳转时将回调作为参数传递到下一页
HMRouterMgr.push({
pageUrl: "hhy://searchpage",
param: { msg: "监听跳转回调", callback:(data:RouterCallback<string>)=>{
// 这里是目标页调用callback时的回调,data是传递过来的参数,data类型可以自定义,支持任意类型
} },
skipAllInterceptor: true
}
}
// 自定义路由参数类型
interface RouterData {
msg?:string,
callback:RouterCallback<string>
}
// 目标页接收参数(上面已讲过)
@State param: HMPageParam = HMRouterMgr.getCurrentParam(HMParamType.all) as HMPageParam
@State data: RouterData = this.param.data as RouterData
// 页面消失的生命周期中执行回调方法,将数据传递回上一页
aboutToDisappear(): void {
this.data.callback("this is search result")
}
方法路由的实现(路由中官方称为“服务”)
方法路由服务名和路由协议保持一致:scheme://模块/功能
基础用法(简单调用)
// 首先需要注册方法,方式:定义一个类,在类里面定义一个要执行的方法(即服务),使用HMService修饰
export class CustomService {
@HMService({ serviceName: 'testConsole' })
testConsole(): void {
console.info('调用服务 testConsole')
}
}
// 然后在用到的地方调用即可
Button("模拟方法路由(基础)").onClick(e => {
HMRouterMgr.request('testConsole') // request里面的参数就是上面定义的方法名称(serviceName)
}).width('90%')
调用后同步获取返回值(常用)
// 流程和基础用法一样,先注册方法,这个方法有返回值(方法的参数和返回值可以为任意类型)
@HMService({ serviceName: 'testFunWithReturn' })
testFunWithReturn(param1: string, param2: string): string {
return `${param1} ${param2}`
}
// 然后在用到的地方调用接收参数即可
Button("模拟方法路由(传参)").onClick(e => {
let ret = HMRouterMgr.request('testFunWithReturn', 'p1', 'p2').data as string // ret就是服务返回的内容
}).width('90%')
调用后异步获取返回值
// 流程和基础用法一样,先注册方法,这个方法有异步的返回值(方法的参数和返回值可以为任意类型,异步的返回值可能来自网络请求或Promise等)
@HMService({ serviceName: 'testAsyncFun', singleton: true })
async asyncFunction(): Promise<string> {
return new Promise((resolve) => {
resolve('我是返回的内容')
})
}
// 然后在用到的地方调用接收参数即可
Button("模拟方法路由(异步)").onClick(e => {
HMRouterMgr.request('testAsyncFun').data.then((res: string) => {
// 异步结果出来后会执行then方法,res就是异步获取的内容
})
}).width('90%')
路由的高级用法
路由拦截
使用@HMInterceptor标签定义拦截器,并实现IHMInterceptor接口
@HMInterceptor({ interceptorName: 'JumpInfoInterceptor', global: true })
export class JumpInfoInterceptor implements IHMInterceptor {
handle(info: HMInterceptorInfo): HMInterceptorAction {
let connectionInfo: string = info.type === 'push' ? 'jump to' : 'back to';
Logger.info(`${info.srcName} ${connectionInfo} ${info.targetName}`)
return HMInterceptorAction.DO_NEXT;
}
}
路由拦截的一个比较重要的功能是全局登录状态判断,如果用户没登录,自动跳转到登录页面
这里的global官方解释为是否为单例。怎么理解呢?
单例页面即表示在一个HMNavigation容器中,只有一个此页面(简单来说就是如果B页面是单例,A跳转B之后,B再跳转B是没反应的)。
路由拦截做登录
在entry的ets中创建interceptors文件夹,在里面创建LoginInterceptor.ets文件(这个拦截放到我的模块中也行)
import { HMInterceptor, HMInterceptorAction, HMInterceptorInfo, HMRouterMgr, IHMInterceptor } from "@hadss/hmrouter";
// HMInterceptor标记当前类做路由拦截,类要实现接口 IHMInterceptor,拦截后执行handle方法
@HMInterceptor({ priority:9, interceptorName: 'LoginInterceptor', global: true })
export class LoginInterceptor implements IHMInterceptor {
isLogin: boolean = false
handle(info: HMInterceptorInfo): HMInterceptorAction {
if (this.isLogin) {
let connectionInfo: string = info.type === 'push' ? 'jump to' : 'back to';
if (info.srcName === "") { // 登录成功进来的,不做任何处理
}
console.log(`LoginInterceptor ${info.srcName} ${connectionInfo} ${info.targetName}`)
return HMInterceptorAction.DO_NEXT
}
else { // 没登录跳转到登录页
let connectionInfo: string = info.type === 'push' ? 'jump to' : 'back to';
console.log(`LoginInterceptor push LoginPage ${info.srcName} ${connectionInfo} ${info.targetName}`)
HMRouterMgr.push({
pageUrl: "hhy://LoginPage",
param: { targetUrl: info.targetName },
skipAllInterceptor: true
})
// 拦截结束,不再执行下一个拦截器,不再执行相关转场和路由栈操作
return HMInterceptorAction.DO_REJECT;
}
}
}
当app启动进入首页的时候走到拦截器中,其他tab不会进入拦截器,如果要所有页面都经过拦截器,需要启动的时候做路由嵌套(后面实例中会介绍)
定义生命周期
使用@HMLifecycle标签定义生命周期处理器,并实现IHMLifecycle接口
@HMLifecycle({ lifecycleName: 'PageDurationLifecycle' })
export class PageDurationLifecycle implements IHMLifecycle {
private time: number = 0;
onShown(ctx: HMLifecycleContext): void {
this.time = new Date().getTime();
}
onHidden(ctx: HMLifecycleContext): void {
const duration = new Date().getTime() - this.time;
Logger.info(`Page ${ctx.navContext?.pathInfo.name} stay ${duration}`);
}
}
可用的生命周期:
onPrepare?(ctx: HMLifecycleContext): void;
onAppear?(ctx: HMLifecycleContext): void;
onDisAppear?(ctx: HMLifecycleContext): void;
onShown?(ctx: HMLifecycleContext): void;
onHidden?(ctx: HMLifecycleContext): void;
onWillAppear?(ctx: HMLifecycleContext): void;
onWillDisappear?(ctx: HMLifecycleContext): void;
onWillShow?(ctx: HMLifecycleContext): void;
onWillHide?(ctx: HMLifecycleContext): void;
onReady?(ctx: HMLifecycleContext): void;
onBackPressed?(ctx: HMLifecycleContext): boolean;
自定义转场动画
通过@HMAnimator标签定义转场动画,并实现IHMAnimator接口
@HMAnimator({ animatorName: 'liveCommentsAnimator' })
export class liveCommentsAnimator implements IHMAnimator {
effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
// 入场动画
enterHandle.start((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '100%'
})
enterHandle.finish((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '0'
})
enterHandle.duration = 500
// 出场动画
exitHandle.start((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '0'
})
exitHandle.finish((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '100%'
})
exitHandle.duration = 500
}
}
使用动画时,目标页的路由传参animator即可
@HMRouter({ pageUrl: '/pageB', lifecycle: 'pageLifecycle', animator: 'pageAnimator' })
@Component
export struct PageB {
}
路由实战案例
为了app启动时就能路由拦截,实现app必须登录后才能使用的场景。下面是路由嵌套的两种实现方式
首页tabs框架搭建
0,先准备测试工程框架
创建首页模块(Static Library : HAR)
首页模块名就叫home,给首页模块MainPage改名为HomePage
注意: 许多人路由跳转不了,就是少了下面这一步!!!
将首页模块依赖到壳工程中 ohpm install features/home/
最后做路由配置,模块的hvigorfile.ts中配置harPlugin
页面展示几个跳转按钮
@HMRouter({ pageUrl: "hhy://homepage", singleton: true })
@Component
export struct HomePage {
build() {
Column({space: 5}){
Button("跳转到搜索模块(不展示tab)").onClick(e=>{
}).width('90%')
Button("跳转到搜索模块(展示tab)").onClick(e=>{
}).width('90%')
}
.width("100%")
.height('100%')
}
}
创建另一个模块
同样的步骤
1,创建一个搜索模块search(Static Library : HAR),MainPage改名为SearchPage,路由定义为
@HMRouter({ pageUrl: "hhy://homepage", singleton: true })
2,做路由配置,模块的hvigorfile.ts中配置harPlugin
3,将模块依赖到壳工程中 ohpm install features/search/
1,比较常用的方式
// Index页面中定义跟导航,指定首页AppPage
HMNavigation({
navigationId: "AppNavigationID",
homePageUrl: "AppPage",
options: {
// 设置动画样式
standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,
// 设置弹框动画样式
dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
// 设置页面navigation的参数,标题栏,工具栏,bar那些
modifier: this.modifier
}
})
// 创建首页 AppPage
build() {
Column() {
// 页面入口处定义导航,目的是让路由拦截,可以做全局登录功能
HMNavigation({
navigationId: "MainNavigationID",
options: {
standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,
dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
modifier: this.modifier
}
}) {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
HomeMainPage()
}
.tabBar(this.tabBuilder('目标', 0, $r('app.media.icon_mubiao_filled'), $r('app.media.icon_mubiao')))
。。。。。。
}
.onChange((index: number) => {
this.currentIndex = index
})
.barMode(BarMode.Fixed)
.scrollable(false)
.animationMode(AnimationMode.NO_ANIMATION)
}
}
.height('100%')
.width('100%')
}
实现原理,app入口文件是Index,index中的HMNavigation指向AppPage,这一步是路由导航过去的,所以会执行路由拦截。tabs整体布局在AppPage中实现即可。
2,更灵活的控制每个tab页面导航的方式(用起来略显复杂)
AppPage中tab改为下面这段代码,实现根HMNavigation底下的tab又嵌套一个HMNavigation
TabContent() {
HMNavigation({
navigationId: "SubNavigationID",
homePageUrl: "hhy://homepage",
options: {
// 设置动画样式
standardAnimator: HMDefaultGlobalAnimator.STANDARD_ANIMATOR,
// 设置弹框动画样式
dialogAnimator: HMDefaultGlobalAnimator.DIALOG_ANIMATOR,
// 设置页面navigation的参数,标题栏,工具栏,bar那些
modifier: this.modifier
}
})
}
首页模块的HomePage页面实现的测试方法如下:
build() {
Column({space: 5}){
// 页面在tab上面盖着
Button("跳转到搜索模块(页面在tab上面盖着)").onClick(e=>{
HMRouterMgr.push({
navigationId: "AppNavigationID",
pageUrl: "hhy://searchpage",
param: { username: "lichanghong" },
skipAllInterceptor: true
})
}).width('90%')
// 页面在tab下面
Button("跳转到搜索模块(页面在tab下面)").onClick(e=>{
HMRouterMgr.push({
navigationId: "SubNavigationID",
pageUrl: "hhy://searchpage",
param: { username: "lichanghong" },
skipAllInterceptor: true
})
}).width('90%')
// 页面在tab下面
Button("跳转到搜索模块(默认navigationId是不可控的,不建议").onClick(e=>{
HMRouterMgr.push({
pageUrl: "hhy://searchpage",
param: { username: "lichanghong" },
skipAllInterceptor: true
})
}).width('90%')
}
.width("100%")
.height('100%')
}
运行起来看效果会发现,嵌套路由可以实现跳转的目标页是覆盖到tab上面还是下面,单层路由是做不到的。
真实开发场景中,我们可以不做路由嵌套,当有需要不去覆盖tab的场景的时候,再对个别功能做嵌套即可,这里仅介绍嵌套路由的特殊能力. 上面这种实现方案会让页面跳转更加灵活,但是每次跳转必须指定navigationId
跳转后页面在tab上
跳转后页面在tab下
不指定navigationId有问题
如何在页面中点击某个按钮切换tab页面
首先在页面中定义tab的当前index,通过使用@Consume
双向监听index的变化,来触发tab的切换 index变化的监听 (在Tab组件页面实现如下代码)
export struct HomePage {
@Consume currentIndex: number
.....
Button("切换tab").onClick(e => {
this.currentIndex = 1
}).width('90%')
}
index变化的监听 (在Tab组件页面实现如下代码)
@Provide @Watch("tabChange") currentIndex: number = 0
private mTabController: TabsController = new TabsController()
tabChange(propname: string): void {
this.mTabController.changeIndex(this.currentIndex)
}
重点是@Provide @Watch("tabChange")
,通过Provide和其他组件保持双向绑定,在通过watch监听变动事件,从而实现自定义时机切换tab
问题汇总
路由导致如法热更新问题
原因是:路由会清除缓存导致无法热更新
解决方法:在项目根目录创建路由编译插件配置文件hmrouter_config.json,将saveGeneratedFile设置为true
注意: scanDir 下面的路径是路由插件扫描的路径,如果服务或者路由不好使,就确认下路由定义是否在这些目录下