ArkTS Navigation 路由实战:从 Router 迁移到 NavPathStack,打造企业级路由体系


为什么要抛弃 @ohos/router?

在 HarmonyOS NEXT(API 12+)之前,大多数开发者都用 @ohos/router 跳转页面:

// 旧写法 —— 字符串路由,无类型安全
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 存放全局路由栈,方便跨模块访问:

// 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

// 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 定义路由表(类型安全的关键)

// 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

// 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 目标页接收参数

// 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 返回值

// 在 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 实现路由拦截器

// 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 中注册拦截器

// 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 登录成功后跳回原页面

// 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

// 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,建议在下一个迭代中开始迁移,拖得越久历史包袱越重。

Logo

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

更多推荐