ArkTS Navigation 路由实战:从 Router 迁移到 NavPathStack,打造企业级路由体系
为什么要抛弃 @ohos/router?
在 HarmonyOS NEXT(API 12+)之前,大多数开发者都用 @ohos/router 跳转页面:
typescript
// 旧写法 ------ 字符串路由,无类型安全
router.push({ url: 'pages/Detail', params: { id: 123 } });
router.back();
这种方式有几个典型痛点:
| 问题 | 具体表现 |
|---|---|
| 无类型安全 | url 字符串写错编译不报错,运行才崩 |
| 参数传递脆弱 | 参数通过 router.getParams() 取,容易 undefined |
| 无法拦截跳转 | 无法做登录守卫、权限校验 |
| 多端适配困难 | 手机/平板/折叠屏布局难统一 |
从 API 9 开始,华为官方已将 Navigation + NavDestination 定为标准路由方案,API 12 后 @ohos/router 进入软性废弃阶段。本文就带你从旧方案完整迁移到新方案,并构建一套带登录守卫的企业级路由体系。
一、Navigation 核心架构
Navigation 的设计思路是「单 Activity + 多 Fragment」------整个应用只有一个 Navigation 根容器,所有页面通过 NavPathStack 管理入栈/出栈:
EntryPage (Navigation)
├── 首页 (NavDestination: name="Home")
├── 详情页 (NavDestination: name="Detail")
├── 设置页 (NavDestination: name="Settings")
└── 登录页 (NavDestination: name="Login")
三个核心组件:
- Navigation:根视图容器,承载 TitleBar + 内容区
- NavDestination :每个「页面」的容器,替代旧的
@Entry @Component - NavPathStack:路由栈管理器,push/pop/replace 都由它来
二、5分钟搭建 Navigation 骨架
2.1 创建全局路由栈
推荐用 AppStorage 存放全局路由栈,方便跨模块访问:
typescript
// src/main/ets/common/router/AppRouter.ets
import { NavPathStack } from '@kit.ArkUI';
// 全局单例路由栈
export const appRouter = new NavPathStack();
// 注册到 AppStorage,供全局使用
AppStorage.setOrCreate('appRouter', appRouter);
2.2 根页面挂载 Navigation
typescript
// src/main/ets/pages/EntryPage.ets
import { appRouter } from '../common/router/AppRouter';
import { routeMap } from '../common/router/RouteMap';
@Entry
@Component
struct EntryPage {
// 从 AppStorage 获取全局路由栈
@StorageLink('appRouter') navStack: NavPathStack = appRouter;
build() {
Navigation(this.navStack) {
// 这里是首页内容(默认展示)
HomeContent()
}
.navDestination(routeMap) // 绑定路由表
.mode(NavigationMode.Stack) // 手机用 Stack,平板可换 Auto
.title('首页')
.hideTitleBar(false)
}
}
2.3 定义路由表(类型安全的关键)
typescript
// src/main/ets/common/router/RouteMap.ets
import { NavDestinationBuilder } from '@kit.ArkUI';
// 路由名称枚举,杜绝字符串魔法值
export enum RouteName {
HOME = 'Home',
DETAIL = 'Detail',
SETTINGS = 'Settings',
LOGIN = 'Login',
PROFILE = 'Profile',
}
// 路由表:name -> 组件构建函数
@Builder
export function routeMap(name: string, param: ESObject) {
if (name === RouteName.DETAIL) {
DetailPage({ itemId: param?.id as number })
} else if (name === RouteName.SETTINGS) {
SettingsPage()
} else if (name === RouteName.LOGIN) {
LoginPage()
} else if (name === RouteName.PROFILE) {
ProfilePage({ userId: param?.userId as string })
}
}
💡 推荐做法 :将路由表的
@Builder函数集中放在一个文件,所有页面组件在这里统一注册,避免循环依赖。
三、页面跳转实战
3.1 基础跳转:push / pop
typescript
// src/main/ets/pages/HomePage.ets
import { appRouter } from '../common/router/AppRouter';
import { RouteName } from '../common/router/RouteMap';
@Component
struct HomeContent {
build() {
Column({ space: 16 }) {
Text('首页').fontSize(24).fontWeight(FontWeight.Bold)
// 跳转详情页,携带参数
Button('查看商品详情')
.onClick(() => {
appRouter.pushPath({
name: RouteName.DETAIL,
param: { id: 10086, title: 'ArkTS实战课程' }
});
})
// 跳转设置页(不携带参数)
Button('去设置')
.onClick(() => {
appRouter.pushPathByName(RouteName.SETTINGS, null);
})
}
.padding(20)
.width('100%')
}
}
3.2 目标页接收参数
typescript
// src/main/ets/pages/DetailPage.ets
interface DetailParam {
id: number;
title: string;
}
@Component
struct DetailPage {
// 方式一:通过组件属性接收(路由表传入)
@Prop itemId: number = 0;
// 方式二:通过 NavDestination 的 onReady 回调获取
@State title: string = '';
@State price: number = 0;
build() {
NavDestination() {
Column() {
Text(`商品ID: ${this.itemId}`)
Text(`标题: ${this.title}`)
Button('返回上一页')
.onClick(() => {
appRouter.pop();
})
Button('返回并传参给上一页')
.onClick(() => {
// pop 时携带结果,类似 Android 的 startActivityForResult
appRouter.pop({ result: 'user_confirmed', itemId: this.itemId });
})
}
.padding(20)
}
.title('商品详情')
.onReady((context: NavDestinationContext) => {
// 在 onReady 中安全获取路由参数
const param = context.pathInfo.param as DetailParam;
this.title = param?.title ?? '';
})
}
}
3.3 接收 pop 返回值
typescript
// 在 HomeContent 中,等待 Detail 页返回结果
Button('查看并等待结果')
.onClick(async () => {
const result = await appRouter.pushPathByName(RouteName.DETAIL, { id: 1 }, true);
// result 就是 detail 页 pop() 时传入的对象
if (result && result.result === 'user_confirmed') {
console.log('用户确认了商品:', result.itemId);
}
})
四、登录守卫:路由拦截器
这是企业级应用的必备能力------未登录用户访问受保护页面时,自动跳转到登录页,登录成功后回到原来想去的页面。
4.1 实现路由拦截器
typescript
// src/main/ets/common/router/AuthGuard.ets
import { appRouter } from './AppRouter';
import { RouteName } from './RouteMap';
// 需要登录才能访问的页面
const PROTECTED_ROUTES: string[] = [
RouteName.PROFILE,
RouteName.SETTINGS,
// ...其他受保护路由
];
export function setupAuthGuard() {
appRouter.setInterception({
willShow: (from: NavDestinationContext | NavBar, to: NavDestinationContext | NavBar,
operation: NavigationOperation, animated: boolean) => {
// 只拦截 NavDestination(页面跳转),跳过 NavBar(顶部导航)
if (typeof to === 'string') {
return;
}
const toName = (to as NavDestinationContext).pathInfo.name;
// 判断目标页是否需要登录
if (PROTECTED_ROUTES.includes(toName)) {
// 检查登录状态(从 AppStorage 或 Preferences 取)
const isLoggedIn = AppStorage.get<boolean>('isLoggedIn') ?? false;
if (!isLoggedIn) {
// 用户未登录,重定向到登录页
// 同时记录原始目标,登录后可以跳回来
AppStorage.setOrCreate('redirectAfterLogin', toName);
// 替换当前跳转目标为登录页
appRouter.pushPathByName(RouteName.LOGIN, null, false);
// 返回 false 取消原跳转
return;
}
}
}
});
}
4.2 在 EntryPage 中注册拦截器
typescript
// EntryPage.ets
import { setupAuthGuard } from '../common/router/AuthGuard';
@Entry
@Component
struct EntryPage {
@StorageLink('appRouter') navStack: NavPathStack = appRouter;
aboutToAppear() {
// 应用启动时注册路由拦截器
setupAuthGuard();
}
build() {
Navigation(this.navStack) {
HomeContent()
}
.navDestination(routeMap)
.mode(NavigationMode.Stack)
}
}
4.3 登录成功后跳回原页面
typescript
// LoginPage.ets 登录成功回调
function onLoginSuccess() {
// 保存登录状态
AppStorage.setOrCreate('isLoggedIn', true);
// 取出登录前的目标页
const redirect = AppStorage.get<string>('redirectAfterLogin');
AppStorage.delete('redirectAfterLogin');
if (redirect) {
// 替换登录页,跳到原目标
appRouter.replacePath({ name: redirect, param: null });
} else {
// 没有重定向目标,直接返回上一页
appRouter.pop();
}
}
五、多级嵌套导航(Tab + Navigation)
实际 App 经常是底部 Tab + 每个 Tab 内部独立导航的架构,这时需要嵌套 NavPathStack:
typescript
// src/main/ets/pages/MainTabPage.ets
// 每个 Tab 拥有独立的路由栈
const homeStack = new NavPathStack();
const shopStack = new NavPathStack();
const mineStack = new NavPathStack();
@Entry
@Component
struct MainTabPage {
@State currentTab: number = 0;
build() {
Tabs({ barPosition: BarPosition.End }) {
// Tab 1: 首页(独立导航栈)
TabContent() {
Navigation(homeStack) {
HomeContent()
}
.navDestination(homeRouteMap)
.hideTitleBar(true)
}
.tabBar(BottomTabBar({ icon: $r('app.media.ic_home'), label: '首页', index: 0 }))
// Tab 2: 商城(独立导航栈)
TabContent() {
Navigation(shopStack) {
ShopContent()
}
.navDestination(shopRouteMap)
.hideTitleBar(true)
}
.tabBar(BottomTabBar({ icon: $r('app.media.ic_shop'), label: '商城', index: 1 }))
// Tab 3: 我的(独立导航栈)
TabContent() {
Navigation(mineStack) {
MineContent()
}
.navDestination(mineRouteMap)
.hideTitleBar(true)
}
.tabBar(BottomTabBar({ icon: $r('app.media.ic_mine'), label: '我的', index: 2 }))
}
.barHeight(60)
.onChange((index: number) => {
this.currentTab = index;
})
}
}
⚠️ 踩坑提醒 :嵌套 Navigation 时,每个 Tab 的 NavPathStack 必须是独立实例 ,不能共用全局的
appRouter,否则 Tab 切换时路由栈会互相干扰。
六、旧 Router 迁移速查表
如果你的项目已有大量 @ohos/router 代码,参考下表快速迁移:
| 旧写法(@ohos/router) | 新写法(Navigation) | 说明 |
|---|---|---|
router.push({ url: 'pages/X' }) |
navStack.pushPathByName('X', null) |
基础跳转 |
router.push({ url: 'pages/X', params: {id: 1} }) |
navStack.pushPath({ name: 'X', param: {id: 1} }) |
携带参数 |
router.back() |
navStack.pop() |
返回上一页 |
router.back({ url: 'pages/Home' }) |
navStack.popToName('Home') |
返回到指定页 |
router.clear() |
navStack.clear() |
清空路由栈 |
router.getLength() |
navStack.size() |
获取栈深度 |
router.getParams() |
context.pathInfo.param(onReady 中) |
获取传入参数 |
router.replace({ url: 'pages/X' }) |
navStack.replacePath({ name: 'X', param: null }) |
替换当前页 |
七、完整项目结构
src/main/ets/
├── common/
│ └── router/
│ ├── AppRouter.ets # 全局路由栈单例
│ ├── RouteMap.ets # 路由表 + 路由名称枚举
│ └── AuthGuard.ets # 路由拦截器(登录守卫)
├── pages/
│ ├── EntryPage.ets # 根页面(Navigation 挂载点)
│ ├── MainTabPage.ets # 底部 Tab 页(含嵌套 Navigation)
│ ├── HomePage.ets # 首页内容
│ ├── DetailPage.ets # 详情页(NavDestination)
│ ├── LoginPage.ets # 登录页(NavDestination)
│ ├── SettingsPage.ets # 设置页(NavDestination)
│ └── ProfilePage.ets # 个人中心(NavDestination,受保护)
└── entryability/
└── EntryAbility.ets # 应用入口
八、踩坑总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
navStack 为 undefined 导致崩溃 |
在 aboutToAppear 之前访问了路由栈 |
用 @StorageLink 确保响应式绑定,或在 onReady 后操作 |
| pop 返回值拿不到 | pushPath 第三个参数 animated 传了 false |
异步等返回值时必须 pushPathByName(name, param, true) |
| 嵌套 Tab 路由互相干扰 | 所有 Tab 共用了同一个 NavPathStack | 每个 Tab 创建独立的 NavPathStack 实例 |
| 拦截器只触发一次 | 在 onReady 之后才调用 setInterception |
在 EntryPage 的 aboutToAppear 中提前注册 |
| NavDestination 页面白屏 | 路由表里没有注册该 name | 检查 routeMap 函数中是否有对应的 if 分支 |
| Router 和 Navigation 混用报错 | 部分旧页面还用 @Entry,被 Navigation 管理后冲突 |
迁移时统一去掉子页面的 @Entry 装饰器,只保留根页面的 @Entry |
总结
Navigation + NavPathStack 是 HarmonyOS NEXT 路由的最终答案:
- 类型安全:RouteName 枚举替代魔法字符串
- 参数可靠:onReady 中统一取参,不再 undefined
- 拦截能力:登录守卫、权限控制一行注册
- 多端适配 :
NavigationMode.Auto自动判断手机/平板布局 - 嵌套友好:Tab 内独立路由栈,互不干扰
如果你的项目还在用 @ohos/router,建议在下一个迭代中开始迁移,拖得越久历史包袱越重。