Navigation 路由表详解
路由表是 Navigation 框架中将"页面名称"映射到"页面组件"的核心机制 。你的所有页面都通过路由表来注册,Navigation 再根据 pushPathByName('name') 中的 name 去路由表中查找并构建对应的组件。1
一、路由表是什么?
arduino
pushPathByName('Detail')
│
▼
┌─────────────────────┐
│ 路由表(映射) │
│ │
│ 'Home' → HomePage │
│ 'Detail' → DetailPage │
│ 'Login' → LoginPage │
└─────────────────────┘
│
▼
构建 DetailPage 组件 → 显示
路由表本质上回答一个问题:"当我用某个名字跳转时,到底要显示哪个页面?"
二、三种路由表方案
| 方案 | 复杂度 | 适用场景 | 懒加载 | 跨包解耦 |
|---|---|---|---|---|
| @Builder 内联映射 | ⭐ | 小型应用,页面少的模块内导航 | ❌ | ❌ |
| 系统路由表 | ⭐⭐ | 中大型应用,多模块(HAR/HSP) | ✅ | ✅ |
| 自定义路由表 | ⭐⭐⭐ | 需要定制路由逻辑的复杂场景 | ✅ | ✅ |
三、方案一:@Builder 内联映射(最简)
3.1 核心代码
直接在 Navigation 所在的 @Entry 中写一个 @Builder 函数,用 if-else 做映射。
typescript
@Entry
@Component
struct MainEntry {
pageStack: NavPathStack = new NavPathStack();
// ⭐ 路由表:一个 @Builder 函数搞定
@Builder
pageMap(name: string, param: Object | undefined) {
if (name === 'Home') {
HomePage()
} else if (name === 'Detail') {
DetailPage({ item: param as ItemData })
} else if (name === 'Search') {
SearchPage({ keyword: param as string })
}
// 这里可以继续扩展更多页面
}
build() {
Navigation(this.pageStack) {
Column() {
Button('去详情').onClick(() => {
this.pageStack.pushPathByName('Detail', { id: 1 });
})
}
}
.navDestination(this.pageMap) // 注册路由表
}
}
3.2 优点与局限
优点:
- 零配置,代码即路由
- 参数类型可直接在调用处推断
- 适合学习和小型项目
局限:
- 所有页面必须在同一个文件或多个文件中静态
import,模块间耦合 - 首屏加载时所有页面组件都会被打包,无法按需加载
- 页面增多后
if-else链变长,难以维护 - 不支持跨 HAR/HSP 模块的路由23
四、方案二:系统路由表(推荐)
系统路由表是官方推荐的中大型项目方案,通过 JSON 配置文件 声明所有路由,Navigation 在运行时自动完成模块加载和页面构建。3
4.1 架构概览
arduino
应用启动
│
├── module.json5 声明路由表路径
│ "routerMap": "$profile:route_map"
│
└── route_map.json 定义路由映射
{ name: "Detail", buildFunction: "DetailBuilder", ... }
│
▼
Navigation 按需动态 import → 构建 Builder → 显示页面
4.2 第一步:创建路由配置文件
在 entry/src/main/resources/base/profile/ 目录下创建 route_map.json:
json
{
"routerMap": [
{
"name": "Home",
"pageSourceFile": "src/main/ets/pages/HomePage.ets",
"buildFunction": "HomePageBuilder",
"data": {
"description": "首页"
}
},
{
"name": "Detail",
"pageSourceFile": "src/main/ets/pages/DetailPage.ets",
"buildFunction": "DetailPageBuilder",
"data": {
"description": "详情页",
"needAuth": false
}
},
{
"name": "Login",
"pageSourceFile": "src/main/ets/pages/LoginPage.ets",
"buildFunction": "LoginPageBuilder",
"data": {
"description": "登录页"
}
}
]
}
字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string | ✅ | 页面唯一标识,pushPathByName 的参数 |
pageSourceFile |
string | ✅ | 页面文件路径(相对 src 目录) |
buildFunction |
string | ✅ | 该页面导出的 @Builder 函数名 |
data |
object | ❌ | 自定义数据,可通过 getConfigInRouteMap 读取 |
4.3 第二步:在 module.json5 中注册
json5
// entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
// ⭐ 关键配置
"routerMap": "$profile:route_map",
// ... 其他配置
}
}
4.4 第三步:编写页面及其 Builder 函数
每个页面文件必须导出一个 @Builder 函数作为"页面入口"。
typescript
// pages/DetailPage.ets
// ⭐ 必须导出的 Builder 函数,函数名与 route_map.json 中一致
@Builder
export function DetailPageBuilder() {
DetailPage()
}
@Component
export struct DetailPage {
// 从 NavDestinationContext 中获取导航栈和参数
private pageStack: NavPathStack | null = null;
private param: Object | undefined = undefined;
build() {
NavDestination() {
Column() {
Text('详情页')
Button('返回')
.onClick(() => {
this.pageStack?.pop();
})
}
}
.title('详情')
.onReady((ctx: NavDestinationContext) => {
// ⭐ 在 onReady 中获取上下文
this.pageStack = ctx.pathStack;
this.param = ctx.pathInfo?.param;
// 也可以通过 ctx 读取路由配置中的 data
console.log('路由描述: ' + ctx.getConfigInRouteMap()?.data?.['description']);
})
}
}
关键设计:
scss
系统路由表的页面加载流程:
pushPathByName('Detail')
│
▼
① 查找 route_map.json → { name: "Detail", pageSourceFile: "...", buildFunction: "DetailPageBuilder" }
│
▼
② 动态 import → 加载 DetailPage.ets
│
▼
③ 调用 export 的 DetailPageBuilder() → 构建页面组件
│
▼
④ NavDestination.onReady() → 获取上下文,完成初始化
4.5 入口页无需 .navDestination()
使用系统路由表时,不要在 Navigation 上调用 .navDestination(),否则会冲突:4
typescript
@Entry
@Component
struct MainEntry {
pageStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pageStack) {
Column() {
Button('去详情')
.onClick(() => {
// 直接用 pushPathByName,系统自动查路由表
this.pageStack.pushPathByName('Detail', { id: 1 });
})
}
}
// ❌ 不要写 .navDestination(this.pageMap)
// 系统路由表已经接管了映射逻辑
.mode(NavigationMode.Stack)
.title('应用')
}
}
4.6 读取路由配置中的自定义数据
typescript
NavDestination() {
// ...
}
.onReady((ctx: NavDestinationContext) => {
// 获取路由表中配置的 data 字段
const config = ctx.getConfigInRouteMap();
const description = config?.data?.['description']; // "详情页"
const needAuth = config?.data?.['needAuth']; // false
// 可以基于配置做业务逻辑
if (needAuth && !isLogin) {
ctx.pathStack.pushPathByName('Login', null);
}
})
4.7 多模块(HAR/HSP)路由表
每个 HAR/HSP 模块都可以拥有自己的 route_map.json,实现路由的模块级自治:
css
entry/
└── src/main/resources/base/profile/route_map.json ← 模块入口路由
feature_detail/
└── src/main/resources/base/profile/route_map.json ← 详情模块路由
feature_user/
└── src/main/resources/base/profile/route_map.json ← 用户模块路由
每个模块的 route_map.json 配置本模块的页面,pushPathByName 时系统会自动查找所有模块的路由表。1
4.8 系统路由表的优点与局限
优点:
- 按需加载(动态 import),首屏加载快
- 跨 HAR/HSP 模块天然解耦
- 配置集中管理,页面增减只改 JSON
- 支持在
data中附加路由元信息
局限:
- 不支持预览器(需在模拟器或真机上测试)
- 页面参数类型丢失(需手动类型断言)
- 无法做复杂的运行时路由逻辑
五、方案三:自定义路由表(高级)
当系统路由表无法满足需求时(如需要运行时动态注册路由、路由守卫、更复杂的参数处理),可以自建路由管理模块。3
5.1 核心思路
scss
自定义路由管理器 (RouterManager)
┌──────────────────────────────────────┐
│ routeMap: Record<string, BuilderFn> │
│ │
注册页面 ───────→ register(name, builder) │
跳转 ──────────→ push(name, param) │
│ ↓ │
│ 动态 import 模块 → 查 routeMap → │
│ 调用 builder → pushPath │
└──────────────────────────────────────┘
5.2 完整实现
typescript
// ==================== 路由管理器 ====================
// router/RouterManager.ets
import { BusinessError } from '@kit.BasicServicesKit';
// 类型定义
type PageBuilder = WrappedBuilder<[object]>;
interface RouteConfig {
name: string; // 路由名称
moduleName: string; // 所属 HAR/HSP 模块名
pagePath: string; // 页面在模块中的路径
builderName: string; // Builder 函数名
meta?: Record<string, Object>; // 路由元信息
}
// 全局路由表
const ROUTE_MAP: Record<string, RouteConfig> = {
'Home': {
name: 'Home',
moduleName: 'entry',
pagePath: 'pages/HomePage',
builderName: 'HomePageBuilder',
},
'Detail': {
name: 'Detail',
moduleName: 'feature_detail',
pagePath: 'pages/DetailPage',
builderName: 'DetailPageBuilder',
meta: { needAuth: false },
},
'Login': {
name: 'Login',
moduleName: 'feature_user',
pagePath: 'pages/LoginPage',
builderName: 'LoginPageBuilder',
},
};
// 已加载的 Builder 缓存
const builderCache: Map<string, PageBuilder> = new Map();
class RouterManager {
private pageStack: NavPathStack | null = null;
// 初始化:注入 NavPathStack
init(stack: NavPathStack) {
this.pageStack = stack;
}
// 获取已注册的所有路由名(用于路由拦截白名单等)
getAllRouteNames(): string[] {
return Object.keys(ROUTE_MAP);
}
// 异步加载目标页面的 Builder
private async loadBuilder(routeName: string): Promise<PageBuilder> {
// 缓存命中
const cached = builderCache.get(routeName);
if (cached) return cached;
const config = ROUTE_MAP[routeName];
if (!config) {
throw new Error(`路由 "${routeName}" 未注册`);
}
try {
// ⭐ 动态 import,按需加载
const module = await import(
`${config.moduleName}/src/main/ets/${config.pagePath}`
);
const builder = module[config.builderName] as PageBuilder;
builderCache.set(routeName, builder);
return builder;
} catch (err) {
const error = err as BusinessError;
throw new Error(`加载路由 "${routeName}" 失败: ${error.message}`);
}
}
// 跳转到指定页面
async push(routeName: string, param?: Object, onPop?: Callback<PopInfo>) {
if (!this.pageStack) {
throw new Error('RouterManager 未初始化,请先调用 init()');
}
// 路由守卫:统一鉴权
const config = ROUTE_MAP[routeName];
if (config?.meta?.['needAuth'] && !AppState.isLogin) {
this.pageStack.pushPathByName('Login', null);
return;
}
// 正常跳转
this.pageStack.pushPathByName(routeName, param, onPop);
}
// 返回
pop(result?: Object) {
this.pageStack?.pop(result);
}
// 返回到指定页面
popTo(name: string) {
this.pageStack?.popToName(name);
}
// 清空栈
clear() {
this.pageStack?.clear();
}
}
// 单例导出
export const routerManager = new RouterManager();
5.3 入口中使用
typescript
// EntryAbility.ets
import { routerManager } from '../router/RouterManager';
@Entry
@Component
struct MainEntry {
pageStack: NavPathStack = new NavPathStack();
@Builder
pageMap(name: string, param: Object | undefined) {
// 这里直接 push 已在 RouterManager 中处理过的路由
// 或者通过 NavDestination 的 .navDestination 做中转
}
aboutToAppear() {
// ⭐ 初始化路由管理器
routerManager.init(this.pageStack);
}
build() {
Navigation(this.pageStack) {
Column()
}
.navDestination(this.pageMap)
}
}
⚠️ 自定义路由表的完整跨包加载涉及
import()动态导入和WrappedBuilder封装,实现较为复杂。团队规模较小或页面不多时,直接使用系统路由表即可。3
六、三种方案对比总结
css
场景 推荐方案
─────────────────────────────────────────
学习 / Demo / 2-3 个页面 方案一 @Builder 内联
单模块中等项目 方案二 系统路由表
多模块(HAR/HSP)中大项目 方案二 系统路由表
需要复杂路由层逻辑 方案三 自定义路由表
决策流程图
css
是否需要跨 HAR/HSP 模块跳转?
├── 否 → 页面 ≤ 5 个?
│ ├── 是 → 方案一 @Builder 内联
│ └── 否 → 方案二 系统路由表
│
└── 是 → 需要运行时动态注册/卸载路由?
├── 是 → 方案三 自定义路由表
└── 否 → 方案二 系统路由表(多模块各有 route_map.json)
七、常见问题
Q1:navDestination 和系统路由表能同时使用吗?
不能。 如果同时使用,navDestination 的优先级更高,系统路由表会失效。使用系统路由表时,删除 .navDestination() 即可。
Q2:系统路由表配置了但跳转失败?
依次检查:
route_map.json语法是否正确(JSON 格式)buildFunction名称是否与导出的@Builder函数名完全一致pageSourceFile路径是否正确(相对于src目录)- 是否在模拟器或真机上测试(预览器不支持系统路由表)
Q3:路由表中 name 的值有什么限制?
无硬性限制,支持中文。但建议使用语义化英文名,且保证全局唯一。
Q4:如何获取当前正在显示的是哪个页面?
typescript
// 监听页面切换
import { uiObserver } from '@kit.ArkUI';
uiObserver.on('navDestinationSwitch', (info: NavDestinationSwitchInfo) => {
console.log(`当前页面: ${info.to}`);
});
路由表的核心价值在于 "名称解耦"------你的跳转代码只需要知道目的地的名字,而不需要知道它存在于哪个文件、哪个模块、如何构建。这正是 Navigation 框架相比 Router 在架构层面的质变。