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 都由它来

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

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

相关推荐
Swift社区4 小时前
System + AI:下一代 鸿蒙App 架构
人工智能·架构·harmonyos
新小梦4 小时前
DevEco Studio修改HarmonyOS为OpenHarmony
harmonyos
IntMainJhy5 小时前
Flutter 三方库 shimmer 的鸿蒙化适配与实战指南
flutter·华为·harmonyos
IntMainJhy6 小时前
Flutter 三方库 flutter_slidable 的鸿蒙化适配与实战指南
flutter·华为·harmonyos
@不误正业7 小时前
HarmonyOS-6.0-AI全栈能力解析-Data-Augmentation-Kit到智能体开发实战
人工智能·华为·harmonyos·开源鸿蒙
HwJack207 小时前
HarmonyOS APP开发玩透 ArkTS 并发编程
华为·harmonyos
前端不太难8 小时前
鸿蒙 App 架构升级:从页面到 System
架构·状态模式·harmonyos
IntMainJhy8 小时前
Flutter 三方库 image_cropper + flutter_image_compress 的鸿蒙化适配与实战指南
flutter·华为·harmonyos