HarmonyOS《柚兔学伴》项目实战05-Navigation 导航与自定义路由管理
HarmonyOS 推荐使用 Navigation + NavPathStack 实现页面导航,相比传统 @ohos.router,它支持更灵活的路由栈管理、动画控制和跨模块路由。柚兔学伴基于 NavPathStack 封装了 PageContext 自定义路由管理器,配合 router_map.json 路由表和 @Builder 页面注册,构建了一套声明式、可回传数据的导航体系。
一、Navigation + NavPathStack 基础
1.1 传统 router 的局限
HarmonyOS 早期的 @ohos.router 提供了 router.pushUrl() / router.back() 等接口,但存在以下局限:
- 路由栈不透明,无法灵活操作中间页面
- 不支持自定义转场动画
- 页面间数据回传需要通过 AppStorage 或全局变量中转
- 无法实现 Tab 内子页面导航
1.2 Navigation 的优势
Navigation + NavPathStack 是官方推荐的新一代导航方案:
| 特性 | @ohos.router | Navigation + NavPathStack |
|---|---|---|
| 路由栈操作 | push / back | push / pop / replace / clear / moveToTop 等 |
| 数据回传 | 无原生支持 | onReturn 回调机制 |
| 转场动画 | 有限 | 自定义动画 |
| 跨模块路由 | 不支持 | routerMap 全局路由表 |
| 导航模式 | 单一 | Stack / Split 自适应 |
二、PageContext 自定义路由管理器
柚兔学伴封装了 PageContext 类,对 NavPathStack 进行了业务层抽象:
// common/src/main/ets/routermanager/PageContext.ets
export interface RouterParam {
routerName: string; // 路由名称(对应 router_map.json 中的 name)
param?: object; // 传递给目标页面的参数
onReturn?: (data?: object) => void; // 返回时的数据回调
}
export interface IPageContext {
openPage(data: RouterParam, animated?: boolean): void;
popPage(animated?: boolean): void;
replacePage(data: RouterParam, animated?: boolean): void;
}
export class PageContext implements IPageContext {
private readonly pathStack: NavPathStack;
private returnCallback?: (data?: object) => void;
constructor() {
this.pathStack = new NavPathStack();
}
public openPage(data: RouterParam, animated: boolean = true): void {
try {
this.returnCallback = data.onReturn; // 保存返回回调
this.pathStack.pushPath({
name: data.routerName,
param: data.param,
}, animated);
} catch (err) {
Logger.error(TAG, `Open Page ${data.routerName} failed.`);
}
}
public popPage(animated: boolean = true, returnData?: object): void {
try {
if (this.returnCallback) {
this.returnCallback(returnData); // 执行返回回调
this.returnCallback = undefined;
}
this.pathStack.pop(animated);
} catch (err) {
Logger.error(TAG, `Pop Page failed.`);
}
}
public replacePage(data: RouterParam, animated: boolean = true): void {
try {
this.pathStack.replacePath({
name: data.routerName,
param: data.param,
}, animated);
} catch (err) {
Logger.error(TAG, `Open Page ${data.routerName} failed.`);
}
}
public get navPathStack(): NavPathStack {
return this.pathStack;
}
}
2.1 openPage:页面跳转
openPage 封装了 pushPath,额外保存了 onReturn 回调:
pageContext.openPage({
routerName: 'ChatPage',
param: { waitingGif: '/girl_wait.gif', speakingGif: '/girl_speak.gif', gender: 'girl' },
onReturn: (data) => {
// 处理 ChatPage 返回的数据
console.info('ChatPage returned:', JSON.stringify(data));
}
});
2.2 popPage:页面返回
popPage 在执行 pathStack.pop() 前,先调用 returnCallback 将数据回传给上一个页面:
// 在 ChatPage 中返回并传递数据
this.pageContext.popPage(true, { lastMessage: '再见!' });
2.3 replacePage:页面替换
replacePage 替换当前栈顶页面,不产生新的历史记录:
pageContext.replacePage({
routerName: 'MainPage',
param: { tab: 1 }
});
三、PageContext 的创建与使用
3.1 全局创建
在 EntryAbility.onWindowStageCreate 中创建并存储到 AppStorage:
// entry/src/main/ets/entryability/EntryAbility.ets
AppStorage.setOrCreate('pageContext', new PageContext());
3.2 在主页中绑定 Navigation
// entry/src/main/ets/pages/HomePage.ets
@Entry
@ComponentV2
struct HomePage {
private pageContext: PageContext = AppStorage.get('pageContext') as PageContext;
private appPathInfo: NavPathStack = this.pageContext.navPathStack;
build() {
Navigation(this.appPathInfo) {
Column() {
this.TabComponent()
CustomTabBar({ currentIndex: this.currentIndex, tabBarChange: (idx) => {
this.currentIndex = idx;
}})
}
.height('100%').width('100%')
}
.hideTitleBar(true)
.mode(NavigationMode.Stack)
.height('100%')
.width('100%')
}
}
关键点:
Navigation(this.appPathInfo)将 NavPathStack 绑定到 Navigation 组件NavigationMode.Stack使用栈管理模式,支持 push/pop 导航hideTitleBar(true)隐藏 Navigation 自带标题栏,使用自定义 TopBar
四、router_map.json 路由表
4.1 路由表配置
每个模块在 src/main/resources/base/profile/router_map.json 中声明自己的路由:
// entry/src/main/resources/base/profile/router_map.json
{
"routerMap": [
{
"name": "MainPage",
"pageSourceFile": "src/main/ets/pages/MainPage.ets",
"buildFunction": "MainPageBuilder"
},
{
"name": "TimerPage",
"pageSourceFile": "src/main/ets/pages/TimerPage.ets",
"buildFunction": "TimerPageBuilder"
}
]
}
// feature/chat/src/main/resources/base/profile/router_map.json
{
"routerMap": [
{
"name": "ChatPage",
"pageSourceFile": "src/main/ets/view/ChatPage.ets",
"buildFunction": "ChatPageBuilder"
},
{
"name": "WordCardPage",
"pageSourceFile": "src/main/ets/view/WordCardPage.ets",
"buildFunction": "WordCardPageBuilder"
},
{
"name": "SentenceCardPage",
"pageSourceFile": "src/main/ets/view/SentenceCardPage.ets",
"buildFunction": "SentenceCardPageBuilder"
}
]
}
字段说明:
- name:路由名称,即
openPage({ routerName: 'ChatPage' })中使用的标识符 - pageSourceFile:页面组件源文件路径
- buildFunction:页面构建函数名,对应
@Builder导出函数
4.2 各模块路由表总览
| 模块 | 路由名称 | 说明 |
|---|---|---|
| entry | MainPage, TimerPage | 主页、计时器页 |
| chat | ChatPage, WordCardPage, SentenceCardPage | 对话页、单词卡页、句子卡页 |
| todo | PoemPage, CopyPage, TextbookPage, BookListPage, PdfViewerPage | 诗词页、抄写页、课本页、书列表页、PDF 阅读页 |
| stroke | StrokePage | 笔画练习页 |
| my | ScorePage, WebPage, ExchangeListPage, SettingPage, AboutPage | 积分页、网页页、兑换列表页、设置页、关于页 |
4.3 module.json5 引用路由表
在 module.json5 中通过 routerMap 字段引用路由表:
// feature/chat/src/main/module.json5
{
"module": {
"name": "chat",
"type": "har",
"routerMap": "$profile:router_map", // 引用路由表
"deviceTypes": ["default", "tablet", "2in1"]
}
}
编译时,各模块的路由表会自动合并到应用的全局路由注册表中,实现跨模块路由跳转。
五、@Builder 页面注册与 NavDestination
5.1 @Builder 导出函数
每个路由页面需要导出一个与 buildFunction 同名的 @Builder 函数:
// feature/chat/src/main/ets/view/ChatPage.ets
@Component
struct ChatPage {
private pageContext: PageContext = AppStorage.get('pageContext') as PageContext;
build() {
NavDestination() {
Column() {
// 页面内容
}
}
.hideTitleBar(true)
.onReady(async (cxt: NavDestinationContext) => {
// 接收路由参数
const data = cxt.pathInfo.param as Record<string, string | number | boolean>;
// 使用参数初始化页面
});
}
}
@Builder
export function ChatPageBuilder() {
ChatPage()
}
关键步骤:
- 页面组件内部使用
NavDestination作为根容器 - 在
NavDestination.onReady中接收路由参数 - 导出
@Builder函数,函数名必须与router_map.json中的buildFunction一致
5.2 参数接收
onReady 回调的 NavDestinationContext 提供了 pathInfo.param 访问路由参数:
.onReady(async (cxt: NavDestinationContext) => {
const data = cxt.pathInfo.param as Record<string, string | number | boolean>;
this.waitingUrl = data.waitingGif as string;
this.speakingUrl = data.speakingGif as string;
if (data.gender === Gender.GIRL) {
this.timbre = 'BV421_streaming';
}
})
参数类型与 openPage 时传入的 param 对象对应。
5.3 页面返回
在子页面中调用 popPage 返回主页:
Image($r('app.media.ic_back_white'))
.width(26)
.onClick(() => {
this.pageContext.popPage(true); // 带动画返回
})
如需回传数据:
this.pageContext.popPage(true, { lastMessage: content });
六、完整导航流程
1. 用户在 HomePage 点击"对话"按钮
│
▼
2. 调用 pageContext.openPage({
routerName: 'ChatPage',
param: { waitingGif, speakingGif, gender },
onReturn: (data) => { 处理返回数据 }
})
│
▼
3. PageContext.openPage()
├── 保存 onReturn 回调到 returnCallback
└── pathStack.pushPath({ name: 'ChatPage', param: {...} })
│
▼
4. Navigation 根据 router_map.json 找到 ChatPageBuilder
├── 调用 ChatPageBuilder() 创建组件
└── ChatPage 的 NavDestination.onReady 接收 param
│
▼
5. 用户在 ChatPage 点击返回按钮
│
▼
6. 调用 pageContext.popPage(true, returnData)
├── 执行 returnCallback(returnData) → 触发 HomePage 的 onReturn
└── pathStack.pop(true) → 转场动画返回
七、与传统 router 的对比
// 传统 @ohos.router 方式
router.pushUrl({ url: 'pages/ChatPage' });
// 无参数回传,无动画控制,路由栈不透明
// Navigation + PageContext 方式
pageContext.openPage({
routerName: 'ChatPage',
param: { gender: 'girl' },
onReturn: (data) => { /* 处理返回数据 */ }
});
// 声明式参数传递、原生数据回传、可控动画、透明路由栈
Navigation 方案的核心优势:
- 路由栈透明:
pathStack支持获取栈长度、遍历路由、移除中间页面 - 数据回传原生:
onReturn回调机制无需全局变量中转 - 跨模块路由:
router_map.json自动合并,feature 模块的页面可直接跳转 - 动画可控:所有导航操作支持
animated参数 - 模式自适应:
NavigationMode.Stack手机单栏,NavigationMode.Split平板双栏
小结
柚兔学伴基于 Navigation + NavPathStack 构建了完整的路由管理体系:PageContext 封装了 openPage/popPage/replacePage 三个核心操作并支持 onReturn 数据回传;router_map.json 实现了声明式路由注册与跨模块路由合并;@Builder + NavDestination 实现了页面组件与路由的解耦。这一方案相比传统 router 更灵活、更强大,是 HarmonyOS 应用导航的最佳实践。
更多推荐


所有评论(0)