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()
}

关键步骤:

  1. 页面组件内部使用 NavDestination 作为根容器
  2. NavDestination.onReady 中接收路由参数
  3. 导出 @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 方案的核心优势:

  1. 路由栈透明pathStack 支持获取栈长度、遍历路由、移除中间页面
  2. 数据回传原生onReturn 回调机制无需全局变量中转
  3. 跨模块路由router_map.json 自动合并,feature 模块的页面可直接跳转
  4. 动画可控:所有导航操作支持 animated 参数
  5. 模式自适应NavigationMode.Stack 手机单栏,NavigationMode.Split 平板双栏

小结

柚兔学伴基于 Navigation + NavPathStack 构建了完整的路由管理体系:PageContext 封装了 openPage/popPage/replacePage 三个核心操作并支持 onReturn 数据回传;router_map.json 实现了声明式路由注册与跨模块路由合并;@Builder + NavDestination 实现了页面组件与路由的解耦。这一方案相比传统 router 更灵活、更强大,是 HarmonyOS 应用导航的最佳实践。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐