一、基于Navigation的路由管理——常见业务功能场景
基于Dialog类型NavDestination,实现弹窗页面跳转返回后弹窗不关闭
NavDestination有两个类型,通过mode属性进行配置,前文介绍的NavDestination均是STANDARD类型。
名称 | 描述 |
---|---|
STANDARD | 标准类型,NavDestination的生命周期跟随NavPathStack栈中标准Destination变化而改变。 |
DIALOG | 默认透明,进出页面栈不影响下层NavDestination的生命周期。 |
弹窗可以通过Dialog来实现。Dialog实现的弹窗可以实现解耦,还可以实现弹窗跳转页面返回,弹窗不关闭等效果(比如隐私弹窗,该弹窗还可以接续跳转到隐私条款页)。
构建Dialog类型的NavDestination,因为Dialog类型页面背景是透明的,为了更好的效果,可以增加一层蒙层。如果要对弹窗组件增加类似移动出现效果,需要在组件中自行实现。
export struct PrivacyDialog {
'pageInfo') pageStack: NavPathStack; (
string = 'Not Agree'; isAgree:
build() {
NavDestination() {
Stack({ alignContent: Alignment.Center }) {
// 蒙层
Column()
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
// 隐私弹窗
Column() {
Text('注册应用账号')
.fontSize(30)
.height('20%')
Text('请您仔细阅读一下协议并同意,我们将全力保护您的个人信息安全,您可以使用账号登录APP。')
.height('40%')
Divider()
Row() {
// 点击隐私条款,跳转到隐私条款页面,并接受隐私条款的返回值,用来刷新页面的同意状态。
Text('《应用隐私政策》')
.onClick(() => {
let pathInfo: NavPathInfo = new NavPathInfo('PrivacyItem', null, (popInfo: PopInfo) => {
this.isAgree = popInfo.result.toString();
});
this.pageStack.pushDestination(pathInfo, true);
})
Text(this.isAgree)
}
.height('20%')
Divider()
// 点击同意/不同意按钮,将状态返回登录页
Row() {
Button('不同意')
.onClick(() => {
this.pageStack.pop('Not Agree', true);
})
.width('30%')
Button('同意')
.onClick(() => {
this.pageStack.pop('Agree', true);
})
.width('30%')
}
.height('20%')
}
.backgroundColor(Color.White)
.height('50%')
.width('80%')
}
}
.hideTitleBar(true)
// 设置Dialog类型
.mode(NavDestinationMode.DIALOG)
}
}
基于页面生命周期监听,实现页面埋点
在应用运维时,需要了解哪些业务功能用户使用频率较高,为了实现此能力,可以在页面显示/隐藏的的时候进行相应的埋点操作,以方便后台进行统计分析。此类诉求可以通过DevNavigation的状态监听实现。
可以在Ability中的onWindowStageCreate方法中通过
uiObserver.on('navDestinationUpdate',(info) => {})方法注册DevNavigation的状态监听,样例代码通过打样日志的方式记录各个DevNavigation显示/隐藏时的状态,在真实业务中可以相应进行替换。
export default class EntryAbility extends UIAbility {
...
onWindowStageCreate(windowStage: window.WindowStage): void {
...
windowStage.getMainWindow((err: BusinessError, data) => {
...
windowClass = data;
// 获取UIContext实例
let uiContext: UIContext = windowClass.getUIContext();
// 获取UIObserver实例
let uiObserver: UIObserver = uiContext.getUIObserver();
// 注册DevNavigation的状态监听.
uiObserver.on('navDestinationUpdate', (info) => {
// NavDestinationState.ON_SHOWN = 0, NavDestinationState.ON_HIDE = 1
if (info.state == 0) {
// NavDestination组件显示时操作
console.info('page ON_SHOWN: ' + info.name.toString());
} else if (info.state === 1) {
// NavDestination组件隐藏时操作
console.info('page ON_HIDE' + info.name.toString());
} else {
// NavDestination组件其他操作
console.info('page state: ' + info.state);
}
});
});
}
}
基于路由拦截实现页面返回弹窗确认
名称 | 描述 |
---|---|
willShow | 页面跳转前拦截,允许操作栈,在当前跳转中生效。 |
didShow | 页面跳转后回调。在该回调中操作栈在下一次跳转中刷新。 |
modeChange | Navigation单双栏显示状态发生变更时触发该回调。 |
如上所述,willShow会在from页面动效完成之前回调;didiShow会在from页面动效完成之后回调。此处需要注意的是无论哪个回调,在进入回调时页面栈都已经发生了变化。
案例中,由于在弹出确认框时,不能出现登录页面返回的转场动画,因此需要在willShow回调中执行相关业务。代码中使用了系统路由表框架,具体实现细节参考下文中路由框架。
代码判断srcPage为登录页面LoginPageView,targetPage为主页面MainPage是执行弹窗逻辑。 由于LoginPageView此时已经出栈,需要重新将LoginPageView重新入栈。此处需要注意需要将参数重新传递给LoginPageView,否则无法顺利完成页面初始化。如果需要保持页面之前的输入值,则需要为组件独立注册LocalStorage以保持状态。 弹窗ConfirmDialog通过Dialog类型的NavDestination实现,启动ComfirmDialog时,将点击OK和Cancel的回调一并传入,从而提高ComfirmDialog的独立封装能力。 当点击‘OK’确认返回时,需要将ConfirmDialog和LoginPageView同时出栈。此时需要注意这一帧的起点栈为Dialog类型,因此不会有转场动画,如果需要转场动画,需要通过自定义动画实现。
export function registerInterception() {
RouterManager.setInterception({
willShow: (from: NavDestinationContext | 'navBar', to: NavDestinationContext | 'navBar',
operation: NavigationOperation, animated: boolean) => {
if (typeof to === 'string') {
console.log('target page is navigation home');
return;
}
if (typeof from === 'string') {
console.log('target page is navigation home');
return;
}
// redirect target page.Change pageTwo to pageOne.
let target: NavDestinationContext = to as NavDestinationContext;
let srcPage: NavDestinationContext = from as NavDestinationContext;
console.log('==== setInterception target.pathInfo.name = ' + target.pathInfo.name);
if (target.pathInfo.name === 'MainPage' && srcPage.pathInfo.name === 'LoginPage') {
RouterManager.pushPath('LoginPage', srcPage.pathInfo.param, srcPage.pathInfo.onPop, true);
let confirmDialogData = new ConfirmDialogData();
confirmDialogData.onCancelClick = () => {
RouterManager.popWithoutParam(false);
}
confirmDialogData.onConfirmClick = () => {
RouterManager.popWithoutParam(true);
RouterManager.popWithoutParam(true);
}
RouterManager.pushPath('confirmDialog', confirmDialogData, () => {
}, false)
}
},
didShow: (from: NavDestinationContext | 'navBar', to: NavDestinationContext | 'navBar',
operation: NavigationOperation, isAnimated: boolean) => {
},
modeChange: (mode: NavigationMode) => {
}
});
}
补充一些有用的知识
let localStorageLoginPage: LocalStorage = new LocalStorage();
localStorageLoginPage.setOrCreate('userName', '');
export struct LoginPageView {
'userName') userName: string = ''; (
string = ''; initUserName:
build() {
NavDestination() {
TextInput({ placeholder: 'User Name', text: this.initUserName })
.onChange((value: string) => {
this.userName = value;
})
.width('80%')
}
.hideTitleBar(true)
.onReady((navDestinationContext) => {
this.loginParam = navDestinationContext.pathInfo.param as LoginParamInHAR;
// 判断页面初始输入框初始阶段显示的指是来自传参还是localStorage
if (this.userName === '') {
this.initUserName = this.loginParam.userName === 'not login' ? '' : this.loginParam.userName;
} else {
this.initUserName = this.userName;
}
})
}
}
export function getLoginPage(): void {
LoginPageView({}, localStorageLoginPage);
}
二、路由框架
系统跨模块路由框架
{
"module" : {
...
"routerMap": "$profile:router_map"
}
}
Step2:在src\main\resources\base\profile(若之前没有profile目录,则需要新建)目录下新建router_map.json文件,此处文件名称需要与module.json5中配置的文件名称一致。
配置项 | 说明 |
---|---|
name | 跳转页面名称,用于路由跳转时使用,如this.pageStack.pushPathByName('MainPage', null, false); |
pageSourceFile | 跳转目标页在包内的路径,相对src目录的相对路径。 |
buildFunction | 跳转目标页的入口函数名称,必须以@Builder修饰。 |
data | 应用自定义字段。可以通过配置项读取接口getConfigInRouteMap获取。 |
{
"routerMap": [
{
"name": "MainPage",
"pageSourceFile": "src/main/ets/components/mainpage/MainPage.ets",
"buildFunction": "getMainPageRouterMap",
"data": {
"description" : "this is mainPage"
}
},
{
"name": "PersonDetail",
"pageSourceFile": "src/main/ets/components/mainpage/PersonDetail.ets",
"buildFunction": "getPersonDetailRouterMap",
"data": {
"description" : "this is PersonDetailPage"
}
}
]
}
Step3:Navigation主页开发:使用系统路由表方案可以不配置.navDestination()属性,如若配置.navDestination()属性,则会先查询.navDestination()中配置的路由,再查询router_map中的路由。
struct IndexRouterMap {
'pageInfo') pageStack: NavPathStack = new NavPathStack(); (
build() {
Navigation(this.pageStack) {
...
}
.onAppear(() => {
this.pageStack.pushPathByName('MainPage', null, false);
})
.hideNavBar(true)
}
}
更进一步,可以进行封装,将pageStack封装到路由管理对象中,避免通过Provide的方式传递pageStack。
struct IndexRouterMap {
private pageStack: NavPathStack = new NavPathStack();
aboutToAppear(): void {
RouterManager.createNavPathStack(this.pageStack);
...
}
build() {
Navigation(this.pageStack) {
...
}
.onAppear(() => {
RouterManager.pushPath('MainPage', null, () => {}, false);
})
.hideNavBar(true)
}
}
Step4:NavDestination页面开发:与不使用路由框架对比,采用路由框架后,需要注意一下两点:
页面需要增加@Builder函数,函数名称与router_map.json中的buildFunction属性必须一致。 在router_map.json中配置的data信息,通过onReady回调中执行navDestinationContext.getConfigInRouteMap()获取。
export struct MainPage {
...
build() {
NavDestination() {
...
}
.hideTitleBar(true)
.onReady(navDestinationContext => {
let config: RouteMapConfig | undefined = navDestinationContext.getConfigInRouteMap();
if (config) {
let value: string = config.data['description'];
...
}
})
}
}
export function getMainPageRouterMap(): void {
MainPage();
}
特别注意
{
...
"devDependencies": {
"@ohos/hypium": "1.0.19",
"@ohos/hamock": "1.0.1-rc2"
}
}
自定义跨模块路由框架
定义个路由管理模块,各个提供路由页面的模块均依赖此模块。
构建Navgation组件时,将NavPactStack注入路由管理模块,路由管理模块对NavPactStack进行封装,对外提供路由能力。
各个路由页面不再提供组件,转为提供@Builder封装的构建函数,并通过WrappedBuilder封装后,实现全局封装。
各个路由页面将模块名称、路由名称、WrappedBuilder封装后构建函数注册如路由模块。
当路由需要跳转到指定路由时,路由模块完成对指定路由模块的动态导入,并完成路由跳转。
三、Router切换Navigation
Navigation基础容器构建
具体步骤可以参考页面间跳转
页面切换为NavDestination组件
转场动画切换
Router相关路由能力替换
Router 能力 | Navigation 能力 |
---|---|
pushUrl | pushDestination |
pushNamedRoute | pushDestination |
replaceUrl | replacePath |
replaceNamedRoute | replacePath |
back | pop |
showAlertBeforeBackPage\hideAlertBeforeBackPage | 可自定义 ,参考生命周期监听 |
clear | clear |
getLength | size |
getState | getAllPathName |
自定义转场动画 | 参考自定义转场动画 |
共享元素动画 | 参考共享元素转场 |
Router路由配置信息从main_pages.json中移除
生命周期切换
路由框架切换
Step1:完成各个页面的Navigation/NavDestination改造。
Step2:页面切换为@Builder构建函数。
Step3:完成系统路由配置,并将NavPathSatck传入路由模块。
Step4:路由框架router相关方法通过NavPathSatck相关方法进行替换。
Step5:给涉及路由的模块配置路由表router_map.json。
四、Navigation常见问题
A:可以混用,但是推荐使用Navigation进行开发。
A:无限制。
A:参数对象可以包含方法,目标页面可以调用对象方法。
A:在组件中执行this.queryNavDestinationInfo()?.name方法,获取NavDestination名称。
A:在组件中执行this.queryNavigationInfo()?.pathStack方法,获取pathStack路由栈对象。
最佳实践官网链接:
https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-harmonyos-features-V5?catalogVersion=V5
FAQ链接:
https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-business-scenarios-and-solutions-V5
开发指南链接:
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/application-dev-guide-V5?catalogVersion=V5
API参考链接:
https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/development-intro-api-V5?catalogVersion=V5