鸿蒙路由与导航
What — 是什么
鸿蒙路由与导航是 HarmonyOS 应用中管理页面跳转、返回、传参和页面栈的核心机制。鸿蒙提供了两套并行的路由方案——Router 模块(轻量级页面路由)与 Navigation 组件(声明式导航容器),配合 Tabs 组件实现多 Tab 导航,共同构成了鸿蒙应用完整的导航体系。
核心概念:
- Router 模块:
@ohos.router提供的基础路由 API,通过pushUrl/replaceUrl/back等方法管理页面栈,适合简单页面跳转场景 - Navigation 组件:ArkUI 声明式导航容器,配合
NavDestination和NavPathStack实现更灵活的导航管理,是鸿蒙推荐的一站式导航方案 - NavPathStack:Navigation 的路由栈对象,提供 push/pop/replace/clear 等栈操作,支持路由拦截与动画定制
- NavDestination:Navigation 内的页面容器组件,每个 NavDestination 代表导航栈中的一个页面
- Tabs 组件:多标签导航容器,配合
TabContent实现底部/顶部 Tab 切换,是大多数 App 的主页导航形态 - 页面栈(Page Stack):系统维护的页面实例栈结构,栈顶为当前可见页面,路由操作本质是栈的 push/pop
- 页面转场动画:
PageTransitionEnter/PageTransitionExit/SharedTransition实现页面间的过渡动画效果 - 深度链接(Deep Linking):通过 URI Scheme 统一跳转,支持跨应用、跨设备的页面直达
鸿蒙路由与导航整体架构:
┌─────────────────────────────────────────────────────────────────────────┐
│ 鸿蒙路由与导航体系架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 应用入口 (EntryAbility) │ │
│ │ (UIAbility.onWindowStageCreate) │ │
│ └───────────────────────┬─────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────┼──────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Router 模块 │ │ Navigation │ │ 多 Ability │ │
│ │ (轻量路由) │ │ 组件导航 │ │ 导航 │ │
│ │ │ │ │ │ │ │
│ │· pushUrl │ │· NavPathStack│ │· Ability │ │
│ │· replaceUrl │ │· NavDest │ │ 连接跳转 │ │
│ │· back │ │· 路由拦截 │ │· Context │ │
│ │· clear │ │· 转场动画 │ │ 启动Ability │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ 页面栈管理 (Page Stack) │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │Page4│ │Page3│ │Page2│ │Page1│ ← 栈顶为当前页面 │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Tabs 导航 │ │ 页面转场动画 │ │ 深度链接 │ │
│ │· TabContent │ │· PageTrans │ │· URI Scheme │ │
│ │· Tab切换 │ │· SharedTrans │ │· App Linking │ │
│ │· 底部导航 │ │· 自定义动画 │ │· 跨应用跳转 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
Router 模块 vs Navigation 组件导航对比:
| 维度 | Router 模块 | Navigation 组件导航 |
|---|---|---|
| 定位 | 轻量级基础路由 API | 一站式声明式导航方案 |
| 适用场景 | 简单页面跳转、小型应用 | 复杂导航、单页面内多级跳转 |
| 页面管理 | 系统页面栈,每个页面独立 AbilitySlice | NavPathStack 自管理,同一 Ability 内 |
| 路由拦截 | 不支持 | 支持 onRouteChange 拦截 |
| 转场动画 | PageTransition 有限支持 | 丰富的 NavDestination 动画定制 |
| 路由守卫 | 无原生支持 | 通过 NavPathStack 拦截实现 |
| 传参方式 | params 对象 | 路由参数 + 共享状态 |
| 官方推荐 | 兼容旧项目 | 鸿蒙推荐方案 |
| 学习曲线 | 低 | 中 |
| 页面生命周期 | @Entry 页面生命周期 | NavDestination 生命周期 |
| 跨 Ability | 直接支持 | 需配合 Context 启动 |
Why — 为什么需要
1. 多页面应用的基础骨架
任何非单页面应用都需要在多个界面之间切换。路由与导航是应用的”骨架”,决定了用户如何在不同功能模块间流转。没有统一的路由方案,页面跳转会变成散落的硬编码调用,难以维护和扩展。
2. 页面栈的合理管理
移动端应用的页面跳转遵循栈结构——新页面入栈,返回时出栈。不当的栈管理会导致内存泄漏(栈无限增长)或用户体验异常(返回到了不期望的页面)。鸿蒙的路由体系提供了栈操作的完整能力,让开发者能精确控制页面栈状态。
3. 统一的导航体验
鸿蒙同时支持手机、平板、车机、手表等多设备,统一的导航体系确保在不同设备形态下用户都能获得一致的导航体验。Navigation 组件天然支持多设备适配,一个导航结构可以灵活适配不同屏幕。
4. 路由鉴权与拦截的安全需求
企业应用中,很多页面需要登录态或权限校验才能访问。通过路由守卫/拦截机制,可以在跳转前统一做权限检查,未授权时跳转登录页,避免在每个页面重复编写鉴权逻辑。
5. 深度链接与跨端跳转
鸿蒙的分布式能力要求应用支持从其他设备、其他应用、语音助手等入口直达特定页面。深度链接(Deep Linking)是实现这一能力的基础,而路由体系需要原生支持 URI 到页面的映射。
6. 转场动画提升用户体验
页面间的切换动画不仅仅是视觉装饰,更是用户空间感知的线索——“前进”动画暗示层级深入,“返回”动画暗示回到上级。合理的转场动画让导航具有方向感,显著提升用户的使用体验。
How — 怎么做
一、Router 模块详解
Router 是鸿蒙最早提供的基础路由模块,通过 @ohos.router 导入使用,API 风格与传统前端路由类似。
1.1 Router 核心 API
// 导入 Router 模块
import router from '@ohos.router';
// ====== 1. pushUrl — 压栈跳转 ======
// 将目标页面压入页面栈,当前页面保留在栈中
router.pushUrl({
url: 'pages/SecondPage', // 目标页面路径(基于 main/ets 目录的相对路径)
params: { // 传递参数对象
userId: 1001,
userName: '张三',
fromPage: 'HomePage'
}
});
// ====== 2. replaceUrl — 替换跳转 ======
// 用目标页面替换当前栈顶页面,当前页面被销毁
// 适用于登录页跳转主页后不需要返回登录页的场景
router.replaceUrl({
url: 'pages/MainPage',
params: {
loginTime: Date.now()
}
});
// ====== 3. back — 返回上一页 ======
// 返回到页面栈的上一个页面
router.back();
// 带参数返回
router.back({
url: 'pages/FirstPage', // 指定返回目标页面
params: { // 返回时携带的参数
result: '操作成功',
code: 200
}
});
// ====== 4. clear — 清空页面栈 ======
// 清除页面栈中所有历史页面,只保留当前页面
// 适用于回到首页后清空中间页面的场景
router.clear();
// ====== 5. getLength — 获取栈中页面数量 ======
let stackLength = router.getLength(); // 返回当前页面栈中的页面数量
console.info(`当前页面栈大小: ${stackLength}`);
// ====== 6. getState — 获取当前页面状态 ======
let currentState = router.getState();
console.info(`当前页面索引: ${currentState.index}`);
console.info(`当前页面名称: ${currentState.name}`);
console.info(`当前页面路径: ${currentState.path}`);
Router API 全景图:
┌──────────────────────────────────────────────────────────┐
│ Router 核心 API │
├──────────────┬───────────────────────────────────────────┤
│ pushUrl │ 压栈跳转,当前页面保留,栈深度 +1 │
├──────────────┼───────────────────────────────────────────┤
│ replaceUrl │ 替换跳转,当前页面销毁,栈深度不变 │
├──────────────┼───────────────────────────────────────────┤
│ back │ 出栈返回,栈深度 -1,可携带返回参数 │
├──────────────┼───────────────────────────────────────────┤
│ clear │ 清空栈,只保留当前页面 │
├──────────────┼───────────────────────────────────────────┤
│ getLength │ 获取当前栈中页面总数 │
├──────────────┼───────────────────────────────────────────┤
│ getState │ 获取当前页面的索引/名称/路径 │
├──────────────┼───────────────────────────────────────────┤
│ showAlert │ 返回前弹出确认对话框(即将废弃) │
├──────────────┼───────────────────────────────────────────┤
│ hideAlert │ 隐藏返回确认对话框(即将废弃) │
├──────────────┼───────────────────────────────────────────┤
│ getParams │ 获取跳转参数(即将废弃,建议用状态变量) │
└──────────────┴───────────────────────────────────────────┘
1.2 Router 页面跳转实战
页面配置(main_pages.json):
{
"src": [
"pages/Index",
"pages/LoginPage",
"pages/HomePage",
"pages/DetailPage",
"pages/ProfilePage",
"pages/SettingsPage"
]
}
首页发起跳转:
// pages/Index.ets
import router from '@ohos.router';
@Entry
@Component
struct Index {
@State message: string = '首页';
build() {
Column() {
Text(this.message)
.fontSize(28)
.fontWeight(FontWeight.Bold)
// 按钮跳转 — pushUrl 压栈
Button('跳转到详情页 (push)')
.width('80%')
.margin({ top: 20 })
.onClick(() => {
router.pushUrl({
url: 'pages/DetailPage',
params: {
itemId: 42,
itemName: '鸿蒙开发指南',
timestamp: Date.now()
}
}).catch((error: Error) => {
console.error(`跳转失败: ${error.message}`);
});
})
// 按钮跳转 — replaceUrl 替换
Button('跳转到主页 (replace)')
.width('80%')
.margin({ top: 10 })
.onClick(() => {
// replaceUrl:当前页面被销毁,用户无法返回到此页面
router.replaceUrl({
url: 'pages/HomePage',
params: {
source: 'Index'
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
目标页面接收参数:
// pages/DetailPage.ets
import router from '@ohos.router';
@Entry
@Component
struct DetailPage {
// 方式1:通过 router.getParams() 获取参数(注意:该方式即将废弃)
// private params = router.getParams();
// 方式2(推荐):在 aboutToAppear 中获取参数并赋值给状态变量
@State itemId: number = 0;
@State itemName: string = '';
@State timestamp: number = 0;
aboutToAppear() {
const params = router.getParams() as Record<string, Object>;
if (params) {
this.itemId = params.itemId as number;
this.itemName = params.itemName as string;
this.timestamp = params.timestamp as number;
}
console.info(`接收到参数: itemId=${this.itemId}, itemName=${this.itemName}`);
}
build() {
Column() {
Text(`详情页 - ${this.itemName}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(`商品ID: ${this.itemId}`)
.fontSize(16)
.margin({ top: 10 })
// 返回按钮 — 携带返回参数
Button('返回首页 (带参数)')
.width('80%')
.margin({ top: 20 })
.onClick(() => {
router.back({
url: 'pages/Index',
params: {
result: '用户已查看详情',
viewedItemId: this.itemId
}
});
})
// 返回按钮 — 简单返回
Button('直接返回')
.width('80%')
.margin({ top: 10 })
.onClick(() => {
router.back();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
1.3 Router 路由模式
Router 提供了两种路由模式,控制页面栈的行为:
import router from '@ohos.router';
// ====== Standard 模式(默认) ======
// 每次跳转都会创建新的目标页面实例压入栈中
// 即使目标页面已在栈中,也会再压入一份
router.pushUrl({
url: 'pages/DetailPage'
}, router.RouterMode.Standard);
// 栈状态:[Index, DetailPage, DetailPage, DetailPage, ...]
// 每次点击都会产生新的 DetailPage 实例
// ====== Single 模式 ======
// 如果目标页面已在栈中,则将栈中该页面之上的所有页面弹出,使其成为栈顶
// 如果目标页面不在栈中,则行为与 Standard 一致
router.pushUrl({
url: 'pages/DetailPage'
}, router.RouterMode.Single);
// 假设当前栈:[Index, HomePage, DetailPage, SettingsPage]
// 执行 Single 模式跳转到 HomePage:
// 结果栈:[Index, HomePage] — SettingsPage 和 DetailPage 被弹出
路由模式选择指南:
┌─────────────────────────────────────────────────────────────┐
│ 路由模式选择决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 目标页面是否应该存在于栈中? │
│ │ │
│ ├── 否 → 使用 Standard 模式(默认) │
│ │ │
│ └── 是 → 是否需要保留中间页面? │
│ │ │
│ ├── 是 → Standard 模式(允许多实例) │
│ │ │
│ └── 否 → Single 模式(回栈到已有实例) │
│ │
│ 典型场景: │
│ · Standard:详情页、表单页、搜索结果页(每次独立) │
│ · Single: 主页、登录页、设置页(全局唯一) │
│ │
└─────────────────────────────────────────────────────────────┘
1.4 路由错误处理
import router from '@ohos.router';
import { BusinessError } from '@ohos.base';
// 完整的路由跳转带错误处理
async function navigateToPage(url: string, params?: Record<string, Object>) {
try {
await router.pushUrl({
url: url,
params: params
}, router.RouterMode.Standard);
console.info(`跳转成功: ${url}`);
} catch (error) {
let err = error as BusinessError;
console.error(`跳转失败: code=${err.code}, message=${err.message}`);
// 常见错误码处理
switch (err.code) {
case 100001: // URI 格式错误
console.error('页面路径格式不正确');
break;
case 100002: // 目标页面不存在
console.error('目标页面不存在,请检查 main_pages.json 配置');
break;
case 100003: // 页面栈已达上限(默认32层)
console.error('页面栈已满,建议使用 replaceUrl 或 clear');
// 降级处理:替换当前页面
router.replaceUrl({ url: url, params: params });
break;
default:
console.error(`未知路由错误: ${err.code}`);
}
}
}
// 使用封装后的跳转方法
navigateToPage('pages/DetailPage', { id: 1 });
二、页面栈管理
页面栈是路由的核心数据结构,理解其运作机制是正确使用路由的前提。
2.1 页面栈操作与状态变化
┌─────────────────────────────────────────────────────────────────────┐
│ 页面栈操作可视化 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 初始状态(应用启动): │
│ ┌──────┐ │
│ │Index │ ← 栈顶/当前页面 │
│ └──────┘ 栈大小: 1 │
│ │
│ pushUrl('pages/HomePage'): │
│ ┌──────┐ │
│ │Home │ ← 新栈顶 │
│ ├──────┤ │
│ │Index │ │
│ └──────┘ 栈大小: 2 │
│ │
│ pushUrl('pages/DetailPage'): │
│ ┌────────┐ │
│ │Detail │ ← 新栈顶 │
│ ├────────┤ │
│ │Home │ │
│ ├────────┤ │
│ │Index │ │
│ └────────┘ 栈大小: 3 │
│ │
│ back(): │
│ ┌──────┐ │
│ │Home │ ← 回到栈顶 │
│ ├──────┤ │
│ │Index │ DetailPage 被销毁 │
│ └──────┘ 栈大小: 2 │
│ │
│ replaceUrl('pages/ProfilePage'): │
│ ┌─────────┐ │
│ │Profile │ ← 替换栈顶 │
│ ├─────────┤ │
│ │Index │ Home 页面被销毁 │
│ └─────────┘ 栈大小: 2(不变) │
│ │
│ clear(): │
│ ┌─────────┐ │
│ │Profile │ ← 仅保留当前页面 │
│ └─────────┘ 栈大小: 1,历史全部清除 │
│ │
└─────────────────────────────────────────────────────────────────────┘
2.2 页面栈管理最佳实践
import router from '@ohos.router';
// ====== 1. 防止页面栈溢出 ======
// 鸿蒙页面栈默认上限为32层,超出会报错
function safePush(url: string, params?: object) {
const MAX_STACK_SIZE = 30; // 安全阈值,留2层余量
if (router.getLength() >= MAX_STACK_SIZE) {
// 栈即将满,使用 replace 替代 push
console.warn('页面栈接近上限,使用 replaceUrl');
router.replaceUrl({ url, params });
} else {
router.pushUrl({ url, params });
}
}
// ====== 2. 返回指定页面 ======
// 场景:多级表单完成后直接回到首页
function backToHome() {
router.back({
url: 'pages/Index' // 指定回到 Index 页面,中间页面全部出栈
});
}
// ====== 3. 重置到首页 ======
// 场景:退出登录后清空所有页面,回到登录页
function resetToLogin() {
// 方式1:replaceUrl + clear 组合
router.clear();
router.replaceUrl({
url: 'pages/LoginPage',
params: { reason: 'logout' }
});
}
// ====== 4. 条件返回 ======
// 场景:根据栈状态决定返回行为
function smartBack(fallbackUrl: string) {
if (router.getLength() > 1) {
// 栈中还有上一页,正常返回
router.back();
} else {
// 栈中只有当前页面,跳转到指定兜底页面
router.replaceUrl({ url: fallbackUrl });
}
}
2.3 页面栈状态监控
@Entry
@Component
struct StackMonitorPage {
@State stackInfo: string = '';
aboutToAppear() {
this.updateStackInfo();
}
// 更新栈信息显示
updateStackInfo() {
const state = router.getState();
this.stackInfo = `栈大小: ${router.getLength()}, ` +
`当前索引: ${state.index}, ` +
`当前路径: ${state.path}`;
}
build() {
Column() {
Text(this.stackInfo)
.fontSize(14)
.padding(10)
.backgroundColor('#f0f0f0')
.width('90%')
Button('Push Detail')
.onClick(() => {
router.pushUrl({ url: 'pages/DetailPage' });
})
Button('Back')
.onClick(() => {
router.back();
})
}
}
}
三、路由传参与回传
3.1 基本参数传递
import router from '@ohos.router';
// ====== 发送方 ======
// 传递简单参数
router.pushUrl({
url: 'pages/DetailPage',
params: {
id: 1001,
name: '鸿蒙手机',
price: 3999.00,
inStock: true
}
});
// 传递复杂对象
interface OrderInfo {
orderId: string;
items: Array<{ name: string; count: number }>;
address: {
city: string;
street: string;
};
}
router.pushUrl({
url: 'pages/OrderDetailPage',
params: {
order: {
orderId: 'ORD-20260511-001',
items: [
{ name: '鸿蒙手机', count: 1 },
{ name: '手机壳', count: 2 }
],
address: {
city: '深圳',
street: '南山区科技园'
}
} as OrderInfo
}
});
// ====== 接收方 ======
@Entry
@Component
struct DetailPage {
@State itemId: number = 0;
@State itemName: string = '';
@State itemPrice: number = 0;
aboutToAppear() {
const params = router.getParams() as Record<string, Object>;
if (params) {
this.itemId = params.id as number;
this.itemName = params.name as string;
this.itemPrice = params.price as number;
}
}
build() {
// ... UI 展示参数
}
}
3.2 回传参数
// ====== 页面A:发起跳转并等待回传 ======
@Entry
@Component
struct PageA {
@State resultMsg: string = '等待回传...';
build() {
Column() {
Text(`回传结果: ${this.resultMsg}`)
.fontSize(18)
Button('跳转到选择页面')
.onClick(() => {
router.pushUrl({
url: 'pages/SelectPage',
params: { requestType: 'city' } // 告诉选择页面需要选什么
});
})
}
}
// 页面重新可见时检查回传参数
onPageShow() {
const params = router.getParams() as Record<string, Object>;
if (params && params.selectedValue) {
this.resultMsg = `选择了: ${params.selectedValue}`;
}
}
}
// ====== 页面B:选择后回传结果 ======
@Entry
@Component
struct SelectPage {
cities: string[] = ['北京', '上海', '广州', '深圳', '杭州'];
build() {
Column() {
Text('请选择城市')
.fontSize(24)
.fontWeight(FontWeight.Bold)
List() {
ForEach(this.cities, (city: string) => {
ListItem() {
Text(city)
.fontSize(18)
.padding(15)
.width('100%')
.onClick(() => {
// 通过 back 携带回传参数
router.back({
url: 'pages/PageA',
params: {
selectedValue: city, // 回传选择结果
selectedTime: Date.now()
}
});
})
}
})
}
}
}
}
3.3 全局状态传参(AppStorage 方式)
// ====== 方式1:AppStorage 全局状态 ======
// 适合跨多页面共享的数据
// 设置方
AppStorage.setOrCreate('currentUser', '张三');
AppStorage.setOrCreate('userToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
AppStorage.setOrCreate('cartCount', 3);
// 接收方 — 使用 @StorageLink 双向绑定
@Entry
@Component
struct CartPage {
@StorageLink('cartCount') cartCount: number = 0;
@StorageLink('currentUser') userName: string = '';
build() {
Column() {
Text(`${this.userName} 的购物车`)
Text(`商品数量: ${this.cartCount}`)
}
}
}
// ====== 方式2:PersistentStorage 持久化 ======
// 数据在应用重启后仍然保留
PersistentStorage.persistProp('hasLoggedIn', false);
PersistentStorage.persistProp('lastVisitedPage', 'Index');
@Entry
@Component
struct SplashPage {
@StorageLink('hasLoggedIn') hasLoggedIn: boolean = false;
@StorageLink('lastVisitedPage') lastPage: string = 'Index';
aboutToAppear() {
if (this.hasLoggedIn) {
router.replaceUrl({ url: `pages/${this.lastPage}` });
} else {
router.replaceUrl({ url: 'pages/LoginPage' });
}
}
}
四、Navigation 组件导航
Navigation 是鸿蒙官方推荐的导航方案,基于声明式 UI 实现更灵活的导航管理。
4.1 Navigation 基础结构
// Navigation 基础结构
@Entry
@Component
struct NavExample {
// 创建 NavPathStack 路由栈
navPathStack: NavPathStack = new NavPathStack();
build() {
// Navigation 是导航容器,管理所有 NavDestination
Navigation(this.navPathStack) {
// 首页内容
Column() {
Text('首页')
.fontSize(24)
Button('跳转到详情')
.onClick(() => {
// 通过 NavPathStack 压栈跳转
this.navPathStack.pushPath({
name: 'DetailPage', // NavDestination 的 name
param: { id: 42 } // 传递参数
});
})
}
}
.mode(NavigationMode.Stack) // 栈模式(单页面展示)
.navDestination(this.buildNavDestination) // 注册 NavDestination 构建器
.title('主标题')
.titleMode(NavigationTitleMode.Mini)
}
// NavDestination 构建器 — 根据路由 name 动态构建页面
@Builder
buildNavDestination(name: string, param: Object) {
if (name === 'DetailPage') {
DetailPage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'SettingsPage') {
SettingsPage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'ProfilePage') {
ProfilePage({ navPathStack: this.navPathStack, param: param })
}
}
}
// 详情页 — 作为 NavDestination 内容
@Component
struct DetailPage {
navPathStack: NavPathStack = new NavPathStack();
param: Object = {};
@State itemId: number = 0;
aboutToAppear() {
const p = this.param as Record<string, Object>;
this.itemId = (p?.id as number) ?? 0;
}
build() {
NavDestination() {
Column() {
Text(`详情页 - ID: ${this.itemId}`)
.fontSize(24)
Button('返回')
.onClick(() => {
this.navPathStack.pop(); // 出栈返回
})
Button('跳转到设置')
.onClick(() => {
this.navPathStack.pushPath({
name: 'SettingsPage',
param: { from: 'DetailPage' }
});
})
}
}
.title('详情页')
.hideBackButton(false) // 显示返回按钮
}
}
Navigation 组件架构:
┌─────────────────────────────────────────────────────────────────┐
│ Navigation 组件架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Navigation 容器 │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 标题栏 (Title Bar) │ │ │
│ │ │ · title / subtitle / titleMode │ │ │
│ │ │ · 菜单项 / 返回按钮 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ NavDestination 内容区 │ │ │
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │ Page A │ │ Page B │ │ Page C │ │ │ │
│ │ │ │ (栈顶) │ │ │ │ (栈底) │ │ │ │
│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 工具栏 (Toolbar) — 可选 │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ NavPathStack 路由栈管理: │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ C │ │ B │ │ A │ ← 栈顶为当前显示的 NavDestination │
│ └─────┘ └─────┘ └─────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 Navigation 模式
Navigation 支持两种模式:
// ====== Stack 模式 ======
// 单页面展示,适合常规 App 的导航模式
// 一次只显示一个 NavDestination,支持 push/pop 操作
Navigation(this.navPathStack) {
// 首页内容
}
.mode(NavigationMode.Stack)
// ====== Split 模式 ======
// 分栏模式,适合平板/大屏设备
// 左侧导航栏 + 右侧内容区
Navigation(this.navPathStack) {
// 首页内容
}
.mode(NavigationMode.Split)
.navContentCover(true) // 小屏时内容区覆盖导航栏
4.3 NavDestination 声明与配置
// NavDestination 是 Navigation 内的页面容器
@Component
struct ProfilePage {
navPathStack: NavPathStack = new NavPathStack();
param: Object = {};
build() {
NavDestination() {
// 页面内容
Column() {
Text('个人中心')
.fontSize(28)
.fontWeight(FontWeight.Bold)
// 页面内容...
}
.width('100%')
.height('100%')
}
.title('个人中心') // 页面标题
.subtitle('用户信息管理') // 副标题
.hideTitleBar(false) // 显示标题栏
.hideBackButton(false) // 显示返回按钮
.navDestinationMode(NavDestinationMode.DIALOG) // 弹窗模式展示
.onShown(() => {
console.info('ProfilePage 页面可见');
})
.onHidden(() => {
console.info('ProfilePage 页面隐藏');
})
.onBackPressed(() => {
// 拦截返回事件,返回 true 表示已处理返回
// 返回 false 表示使用默认返回行为
console.info('用户点击了返回');
return false;
})
}
}
4.4 NavPathStack 路由栈操作详解
@Entry
@Component
struct NavStackDemo {
navPathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.navPathStack) {
Column() {
// ====== 1. pushPath — 压栈跳转 ======
Button('Push Detail')
.onClick(() => {
this.navPathStack.pushPath({
name: 'DetailPage',
param: { id: 1 }
});
})
// ====== 2. pushPathByName — 按 name 压栈 ======
Button('PushByName')
.onClick(() => {
this.navPathStack.pushPathByName('SettingsPage', { tab: 'privacy' });
})
// ====== 3. pushPath 带回调 ======
Button('Push with Result')
.onClick(() => {
this.navPathStack.pushPath({
name: 'SelectPage',
param: { type: 'city' },
onPop: (popInfo: PopInfo) => {
// 接收回传参数
console.info(`回传结果: ${JSON.stringify(popInfo.result)}`);
}
});
})
// ====== 4. replacePath — 替换栈顶 ======
Button('Replace Top')
.onClick(() => {
this.navPathStack.replacePath({
name: 'LoginPage',
param: { reason: 'token_expired' }
});
})
// ====== 5. pop — 出栈返回 ======
Button('Pop')
.onClick(() => {
this.navPathStack.pop();
})
// ====== 6. popToName — 返回到指定页面 ======
Button('Pop To Home')
.onClick(() => {
this.navPathStack.popToName('HomePage');
})
// ====== 7. popToIndex — 返回到指定索引 ======
Button('Pop To Index 0')
.onClick(() => {
this.navPathStack.popToIndex(0); // 回到栈底
})
// ====== 8. clear — 清空栈 ======
Button('Clear Stack')
.onClick(() => {
this.navPathStack.clear();
})
// ====== 9. removeByIndexes — 移除指定索引的页面 ======
Button('Remove Middle')
.onClick(() => {
// 移除索引为1和2的页面(中间页面)
this.navPathStack.removeByIndexes([1, 2]);
})
// ====== 10. removeByName — 移除指定名称的页面 ======
Button('Remove Settings')
.onClick(() => {
this.navPathStack.removeByName('SettingsPage');
})
// ====== 11. getAllPathName — 获取栈中所有页面名称 ======
Button('Show Stack')
.onClick(() => {
const names = this.navPathStack.getAllPathName();
console.info(`栈中页面: ${names.join(' -> ')}`);
console.info(`栈大小: ${this.navPathStack.size()}`);
})
// ====== 12. getParent — 获取上一级页面信息 ======
Button('Show Parent')
.onClick(() => {
const parent = this.navPathStack.getParent();
if (parent) {
console.info(`上一级页面: ${parent.name}`);
}
})
}
}
.mode(NavigationMode.Stack)
.navDestination(this.buildNavDestination)
}
@Builder
buildNavDestination(name: string, param: Object) {
// 根据 name 构建对应的 NavDestination
if (name === 'DetailPage') {
DetailPage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'SettingsPage') {
SettingsPage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'SelectPage') {
SelectPage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'LoginPage') {
LoginPage({ navPathStack: this.navPathStack, param: param })
}
}
}
NavPathStack 操作全景:
┌───────────────────────────────────────────────────────────────────────┐
│ NavPathStack 完整 API │
├─────────────────────┬─────────────────────────────────────────────────┤
│ pushPath │ 压入新页面到栈顶,支持 onPop 回调 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ pushPathByName │ 按 name 压栈(无需构造 PathInfo 对象) │
├─────────────────────┼─────────────────────────────────────────────────┤
│ replacePath │ 替换栈顶页面 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ replacePathByName │ 按 name 替换栈顶 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ pop │ 弹出栈顶,返回上一页 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ popToName │ 弹出到指定 name 的页面 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ popToIndex │ 弹出到指定索引的页面 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ clear │ 清空整个路由栈 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ removeByIndexes │ 按索引批量移除页面(不触发转场动画) │
├─────────────────────┼─────────────────────────────────────────────────┤
│ removeByName │ 按 name 移除页面(不触发转场动画) │
├─────────────────────┼─────────────────────────────────────────────────┤
│ getAllPathName │ 获取栈中所有页面 name 列表 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ size │ 获取栈大小 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ getParent │ 获取上一级页面信息 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ getParamByName │ 获取指定 name 页面的参数 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ getParamByIndex │ 获取指定索引页面的参数 │
├─────────────────────┼─────────────────────────────────────────────────┤
│ isEmpty │ 判断栈是否为空 │
└─────────────────────┴─────────────────────────────────────────────────┘
4.5 NavPathStack 带回传参数的跳转
// ====== 发送方 ======
// 使用 pushPath 的 onPop 回调接收返回值
this.navPathStack.pushPath({
name: 'CityPickerPage',
param: { currentCity: '北京' },
onPop: (popInfo: PopInfo) => {
// popInfo.result 就是 CityPickerPage 传回的数据
const result = popInfo.result as Record<string, Object>;
this.selectedCity = result.city as string;
console.info(`用户选择了: ${this.selectedCity}`);
}
});
// ====== 接收方(CityPickerPage) ======
@Component
struct CityPickerPage {
navPathStack: NavPathStack = new NavPathStack();
param: Object = {};
build() {
NavDestination() {
Column() {
Text('选择城市')
.fontSize(24)
List() {
ForEach(['北京', '上海', '广州', '深圳'], (city: string) => {
ListItem() {
Text(city)
.onClick(() => {
// 通过 pop 回传数据
this.navPathStack.pop({ city: city, timestamp: Date.now() });
})
}
})
}
}
}
.title('选择城市')
}
}
五、路由守卫与鉴权拦截
5.1 Navigation 路由拦截
Navigation 提供了 onRouteChange 回调,可以在路由变化时进行拦截判断:
@Entry
@Component
struct AuthNavDemo {
navPathStack: NavPathStack = new NavPathStack();
@StorageLink('isLoggedIn') isLoggedIn: boolean = false;
// 需要登录才能访问的页面列表
private authRequiredPages: string[] = [
'ProfilePage',
'OrderPage',
'SettingsPage',
'CartPage'
];
// 检查是否需要登录
private isAuthRequired(pageName: string): boolean {
return this.authRequiredPages.includes(pageName);
}
// 封装安全跳转方法
navigateTo(name: string, param?: Object) {
if (this.isAuthRequired(name) && !this.isLoggedIn) {
// 未登录,跳转到登录页,登录后继续原目标
this.navPathStack.pushPath({
name: 'LoginPage',
param: {
redirectPage: name, // 记录原目标页面
redirectParam: param // 记录原目标参数
}
});
return;
}
// 已登录,正常跳转
this.navPathStack.pushPath({ name, param });
}
build() {
Navigation(this.navPathStack) {
Column() {
Text('首页')
.fontSize(24)
Button('个人中心(需登录)')
.onClick(() => {
this.navigateTo('ProfilePage');
})
Button('关于页面(无需登录)')
.onClick(() => {
this.navigateTo('AboutPage');
})
Button('登录')
.onClick(() => {
this.navigateTo('LoginPage');
})
}
}
.mode(NavigationMode.Stack)
.navDestination(this.buildNavDestination)
}
@Builder
buildNavDestination(name: string, param: Object) {
if (name === 'LoginPage') {
LoginPage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'ProfilePage') {
ProfilePage({ navPathStack: this.navPathStack, param: param })
} else if (name === 'AboutPage') {
AboutPage({ navPathStack: this.navPathStack })
}
}
}
5.2 登录页实现(登录后重定向)
@Component
struct LoginPage {
navPathStack: NavPathStack = new NavPathStack();
param: Object = {};
@State username: string = '';
@State password: string = '';
build() {
NavDestination() {
Column() {
Text('登录')
.fontSize(28)
.fontWeight(FontWeight.Bold)
TextInput({ placeholder: '用户名' })
.width('80%')
.onChange((value) => { this.username = value; })
TextInput({ placeholder: '密码' })
.width('80%')
.type(InputType.Password)
.onChange((value) => { this.password = value; })
Button('登录')
.width('80%')
.onClick(() => {
this.doLogin();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
.title('登录')
.hideBackButton(true) // 登录页隐藏返回按钮
}
private doLogin() {
// 模拟登录请求
// 实际项目中应调用后端 API
const loginSuccess = this.username === 'admin' && this.password === '123456';
if (loginSuccess) {
// 更新全局登录状态
AppStorage.setOrCreate('isLoggedIn', true);
AppStorage.setOrCreate('currentUser', this.username);
// 检查是否有重定向页面
const p = this.param as Record<string, Object>;
const redirectPage = p?.redirectPage as string;
const redirectParam = p?.redirectParam;
if (redirectPage) {
// 有重定向目标:替换登录页,跳转到原目标页面
this.navPathStack.replacePath({
name: redirectPage,
param: redirectParam
});
} else {
// 无重定向目标:回到首页
this.navPathStack.pop();
}
} else {
// 登录失败,提示用户
console.error('登录失败:用户名或密码错误');
}
}
}
5.3 Router 模块的路由守卫(手动实现)
Router 模块没有原生的路由守卫能力,需要通过中间层封装实现:
// router_guard.ets — 路由守卫封装
import router from '@ohos.router';
// 路由守卫配置
interface RouteGuardConfig {
path: string; // 页面路径
requiresAuth: boolean; // 是否需要登录
allowedRoles?: string[]; // 允许的角色(可选)
}
// 路由守卫注册表
const routeGuardRegistry: Map<string, RouteGuardConfig> = new Map([
['pages/ProfilePage', { path: 'pages/ProfilePage', requiresAuth: true }],
['pages/OrderPage', { path: 'pages/OrderPage', requiresAuth: true, allowedRoles: ['user', 'vip'] }],
['pages/AdminPage', { path: 'pages/AdminPage', requiresAuth: true, allowedRoles: ['admin'] }],
['pages/HomePage', { path: 'pages/HomePage', requiresAuth: false }],
['pages/AboutPage', { path: 'pages/AboutPage', requiresAuth: false }],
]);
// 前置守卫 — 在跳转前执行
type BeforeGuard = (to: string, params?: Object) => boolean | string;
// 返回 true:允许跳转
// 返回 false:阻止跳转
// 返回 string:重定向到指定页面
const beforeGuards: BeforeGuard[] = [];
// 注册前置守卫
export function beforeEach(guard: BeforeGuard) {
beforeGuards.push(guard);
}
// 执行前置守卫链
function runBeforeGuards(to: string, params?: Object): boolean | string {
for (const guard of beforeGuards) {
const result = guard(to, params);
if (result !== true) {
return result; // 守卫拦截,返回重定向路径或 false
}
}
return true; // 所有守卫通过
}
// 安全路由跳转方法
export async function guardedPush(url: string, params?: Object): Promise<void> {
// 1. 执行前置守卫
const guardResult = runBeforeGuards(url, params);
if (guardResult === true) {
// 守卫通过,正常跳转
await router.pushUrl({ url, params });
} else if (typeof guardResult === 'string') {
// 守卫重定向
await router.replaceUrl({
url: guardResult,
params: { redirectFrom: url, redirectParams: params }
});
}
// guardResult === false:阻止跳转,什么都不做
}
// ====== 使用示例 ======
// 注册全局前置守卫
beforeEach((to, params) => {
const config = routeGuardRegistry.get(to as string);
if (!config) return true; // 未注册的页面默认放行
if (config.requiresAuth) {
const isLoggedIn = AppStorage.get<boolean>('isLoggedIn') ?? false;
if (!isLoggedIn) {
return 'pages/LoginPage'; // 重定向到登录页
}
// 检查角色权限
if (config.allowedRoles) {
const userRole = AppStorage.get<string>('userRole') ?? 'guest';
if (!config.allowedRoles.includes(userRole)) {
console.warn(`无权限访问: ${to}`);
return false; // 无权限,阻止跳转
}
}
}
return true; // 放行
});
// 使用守卫路由跳转
guardedPush('pages/ProfilePage', { userId: 1 });
guardedPush('pages/OrderPage', { orderId: 'ORD-001' });
guardedPush('pages/HomePage');
六、Tab 导航实战
Tabs 是鸿蒙中最常用的多标签导航组件,绝大多数 App 的主页都采用底部 Tab 导航。
6.1 Tabs 组件基础
@Entry
@Component
struct TabsDemo {
@State currentIndex: number = 0;
// Tab 配置数据
private tabItems: TabItem[] = [
{ title: '首页', icon: $r('app.media.ic_home'), activeIcon: $r('app.media.ic_home_active') },
{ title: '分类', icon: $r('app.media.ic_category'), activeIcon: $r('app.media.ic_category_active') },
{ title: '购物车', icon: $r('app.media.ic_cart'), activeIcon: $r('app.media.ic_cart_active') },
{ title: '我的', icon: $r('app.media.ic_profile'), activeIcon: $r('app.media.ic_profile_active') }
];
build() {
Column() {
// Tabs 容器
Tabs({ index: this.currentIndex }) {
// Tab 1:首页
TabContent() {
HomePage()
}
.tabBar(this.TabBuilder(0, this.tabItems[0]))
// Tab 2:分类
TabContent() {
CategoryPage()
}
.tabBar(this.TabBuilder(1, this.tabItems[1]))
// Tab 3:购物车
TabContent() {
CartPage()
}
.tabBar(this.TabBuilder(2, this.tabItems[2]))
// Tab 4:我的
TabContent() {
ProfilePage()
}
.tabBar(this.TabBuilder(3, this.tabItems[3]))
}
.vertical(false) // 水平方向(底部导航)
.scrollable(false) // 禁止滑动切换
.barMode(BarMode.Fixed) // 固定模式,等宽分布
.barPosition(BarPosition.End) // Tab 栏在底部
.onChange((index: number) => {
this.currentIndex = index; // 更新当前索引
})
.animationDuration(300) // 切换动画时长
}
.width('100%')
.height('100%')
}
// 自定义 TabBar 构建器
@Builder
TabBuilder(index: number, item: TabItem) {
Column() {
Image(this.currentIndex === index ? item.activeIcon : item.icon)
.width(24)
.height(24)
.objectFit(ImageFit.Contain)
Text(item.title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#ff0000' : '#999999')
.margin({ top: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
// Tab 配置接口
interface TabItem {
title: string;
icon: Resource;
activeIcon: Resource;
}
6.2 底部导航完整实战
// ====== 完整底部导航实现(带角标和中间凸起按钮) ======
@Entry
@Component
struct MainTabBar {
@State currentIndex: number = 0;
@StorageLink('cartCount') cartCount: number = 0;
build() {
Column() {
Tabs({ index: $$this.currentIndex }) {
// Tab 1:首页
TabContent() {
HomeTabContent()
}
.tabBar(this.buildTabBar(0, '首页', $r('app.media.ic_home'), $r('app.media.ic_home_filled')))
// Tab 2:发现
TabContent() {
DiscoverTabContent()
}
.tabBar(this.buildTabBar(1, '发现', $r('app.media.ic_discover'), $r('app.media.ic_discover_filled')))
// Tab 3:购物车(带角标)
TabContent() {
CartTabContent()
}
.tabBar(this.buildCartTabBar(2))
// Tab 4:消息(带角标)
TabContent() {
MessageTabContent()
}
.tabBar(this.buildBadgeTabBar(3, '消息', $r('app.media.ic_msg'), $r('app.media.ic_msg_filled'), 5))
// Tab 5:我的
TabContent() {
ProfileTabContent()
}
.tabBar(this.buildTabBar(4, '我的', $r('app.media.ic_user'), $r('app.media.ic_user_filled')))
}
.vertical(false)
.scrollable(false)
.barMode(BarMode.Fixed)
.barPosition(BarPosition.End)
.onChange((index: number) => {
this.currentIndex = index;
})
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
}
// 普通TabBar
@Builder
buildTabBar(index: number, title: string, icon: Resource, activeIcon: Resource) {
Column() {
Image(this.currentIndex === index ? activeIcon : icon)
.width(24)
.height(24)
Text(title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#e4393c' : '#666666')
.margin({ top: 2 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
// 购物车TabBar(带数量角标)
@Builder
buildCartTabBar(index: number) {
Badge({
count: this.cartCount,
maxCount: 99,
position: BadgePosition.RightTop,
style: { fontSize: 10, badgeSize: 16, badgeColor: '#e4393c' }
}) {
Column() {
Image(this.currentIndex === index ? $r('app.media.ic_cart_filled') : $r('app.media.ic_cart'))
.width(24)
.height(24)
Text('购物车')
.fontSize(10)
.fontColor(this.currentIndex === index ? '#e4393c' : '#666666')
.margin({ top: 2 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
}
// 带角标的TabBar
@Builder
buildBadgeTabBar(index: number, title: string, icon: Resource, activeIcon: Resource, badgeCount: number) {
Badge({
count: badgeCount,
maxCount: 99,
position: BadgePosition.RightTop,
style: { fontSize: 10, badgeSize: 16, badgeColor: '#e4393c' }
}) {
Column() {
Image(this.currentIndex === index ? activeIcon : icon)
.width(24)
.height(24)
Text(title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#e4393c' : '#666666')
.margin({ top: 2 })
}
.width('100%')
.height(50)
.justifyContent(FlexAlign.Center)
}
}
}
6.3 Tabs + Navigation 组合导航
在 Tab 内部使用 Navigation 实现二级页面跳转,这是最常见的实战架构:
@Entry
@Component
struct TabNavCombo {
@State currentIndex: number = 0;
// 每个 Tab 拥有独立的 NavPathStack
homeNavStack: NavPathStack = new NavPathStack();
categoryNavStack: NavPathStack = new NavPathStack();
cartNavStack: NavPathStack = new NavPathStack();
profileNavStack: NavPathStack = new NavPathStack();
build() {
Tabs({ index: $$this.currentIndex }) {
// ====== Tab 1:首页 ======
TabContent() {
Navigation(this.homeNavStack) {
Column() {
Text('首页')
.fontSize(24)
Button('查看商品详情')
.onClick(() => {
this.homeNavStack.pushPath({
name: 'ProductDetailPage',
param: { productId: 1001 }
});
})
}
}
.mode(NavigationMode.Stack)
.navDestination(this.homeNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(0, '首页'))
// ====== Tab 2:分类 ======
TabContent() {
Navigation(this.categoryNavStack) {
Column() {
Text('分类')
.fontSize(24)
}
}
.mode(NavigationMode.Stack)
.navDestination(this.categoryNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(1, '分类'))
// ====== Tab 3:购物车 ======
TabContent() {
Navigation(this.cartNavStack) {
Column() {
Text('购物车')
.fontSize(24)
}
}
.mode(NavigationMode.Stack)
.navDestination(this.cartNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(2, '购物车'))
// ====== Tab 4:我的 ======
TabContent() {
Navigation(this.profileNavStack) {
Column() {
Text('我的')
.fontSize(24)
Button('我的订单')
.onClick(() => {
this.profileNavStack.pushPath({
name: 'OrderListPage',
param: { userId: 1001 }
});
})
}
}
.mode(NavigationMode.Stack)
.navDestination(this.profileNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(3, '我的'))
}
.vertical(false)
.scrollable(false)
.barMode(BarMode.Fixed)
.barPosition(BarPosition.End)
}
// 各 Tab 的 NavDestination 构建器
@Builder
homeNavDest(name: string, param: Object) {
if (name === 'ProductDetailPage') {
ProductDetailPage({ navPathStack: this.homeNavStack, param: param })
}
}
@Builder
categoryNavDest(name: string, param: Object) {
// 分类页二级页面
}
@Builder
cartNavDest(name: string, param: Object) {
// 购物车二级页面
}
@Builder
profileNavDest(name: string, param: Object) {
if (name === 'OrderListPage') {
OrderListPage({ navPathStack: this.profileNavStack, param: param })
}
}
@Builder
buildTabBar(index: number, title: string) {
Column() {
Text(title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#e4393c' : '#666666')
}
}
}
Tabs + Navigation 组合架构图:
┌──────────────────────────────────────────────────────────────────┐
│ Tabs + Navigation 组合架构 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Tabs 容器 │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Nav(A) │ │ Nav(B) │ │ Nav(C) │ │ Nav(D) │ │ │
│ │ │ Stack_A │ │ Stack_B │ │ Stack_C │ │ Stack_D │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ ┌──┐ │ │ ┌──┐ │ │ ┌──┐ │ │ ┌──┐ │ │ │
│ │ │ │A2│ │ │ │B2│ │ │ │C2│ │ │ │D2│ │ │ │
│ │ │ ├──┤ │ │ ├──┤ │ │ ├──┤ │ │ ├──┤ │ │ │
│ │ │ │A1│ │ │ │B1│ │ │ │C1│ │ │ │D1│ │ │ │
│ │ │ └──┘ │ │ └──┘ │ │ └──┘ │ │ └──┘ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ ┌────────┬────────┬────────┬────────┐ │ │
│ │ │ 首页 │ 分类 │ 购物车 │ 我的 │ ← TabBar │ │
│ │ └────────┴────────┴────────┴────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 关键点: │
│ 1. 每个 Tab 拥有独立的 NavPathStack,互不干扰 │
│ 2. 切换 Tab 时,各 Tab 的导航栈状态保持 │
│ 3. Tab 内可自由 push/pop 二级页面 │
│ 4. 返回键优先处理当前 Tab 的 NavPathStack │
│ │
└──────────────────────────────────────────────────────────────────┘
七、页面转场动画
7.1 PageTransitionEnter / PageTransitionExit
// 页面级转场动画 — 在 @Entry 页面中使用
@Entry
@Component
struct PageTransitionDemo {
// ====== 页面进入动画 ======
pageTransition(): void {
// 定义进入动画
PageTransitionEnter({ duration: 500, curve: Curve.EaseInOut })
.slide(SlideEffect.Right) // 从右侧滑入
.opacity(1.0) // 淡入
// 定义退出动画
PageTransitionExit({ duration: 500, curve: Curve.EaseInOut })
.slide(SlideEffect.Left) // 向左侧滑出
.opacity(0.0) // 淡出
}
build() {
Column() {
Text('页面转场动画示例')
.fontSize(24)
Button('跳转下一页')
.onClick(() => {
router.pushUrl({ url: 'pages/SecondPage' });
})
}
.width('100%')
.height('100%')
}
}
// ====== 目标页面的转场动画 ======
@Entry
@Component
struct SecondPage {
pageTransition(): void {
// 进入:从右滑入
PageTransitionEnter({ duration: 400 })
.slide(SlideEffect.Right)
// 退出:向右滑出(表示"返回上级")
PageTransitionExit({ duration: 400 })
.slide(SlideEffect.Right)
}
build() {
Column() {
Text('第二页')
.fontSize(24)
Button('返回')
.onClick(() => {
router.back();
})
}
.width('100%')
.height('100%')
}
}
7.2 自定义转场动画
@Entry
@Component
struct CustomTransitionPage {
// ====== 自定义缩放 + 透明度进入动画 ======
pageTransition(): void {
PageTransitionEnter({ duration: 600, curve: Curve.FastOutSlowIn })
.onEnter((type: RouteType, progress: number) => {
// type: RouteType.Push 或 RouteType.Pop
// progress: 0.0 ~ 1.0 动画进度
if (type === RouteType.Push) {
// Push 进入:从缩小状态放大到正常
this.scaleX = progress;
this.scaleY = progress;
this.opacityVal = progress;
} else {
// Pop 返回:从左侧滑入
this.translateX = (1 - progress) * -300;
this.opacityVal = progress;
}
})
PageTransitionExit({ duration: 600, curve: Curve.FastOutSlowIn })
.onExit((type: RouteType, progress: number) => {
if (type === RouteType.Push) {
// Push 时当前页退出:向左滑出
this.translateX = progress * -300;
this.opacityVal = 1 - progress;
} else {
// Pop 时当前页退出:缩小消失
this.scaleX = 1 - progress;
this.scaleY = 1 - progress;
this.opacityVal = 1 - progress;
}
})
}
@State scaleX: number = 1;
@State scaleY: number = 1;
@State opacityVal: number = 1;
@State translateX: number = 0;
build() {
Column() {
Text('自定义转场动画')
.fontSize(24)
}
.width('100%')
.height('100%')
.scale({ x: this.scaleX, y: this.scaleY })
.opacity(this.opacityVal)
.translate({ x: this.translateX })
}
}
7.3 SharedTransition 共享元素转场
共享元素转场(Hero 动画)让两个页面中的相同元素产生视觉连续性:
// ====== 页面A:列表页 ======
@Entry
@Component
struct ListPage {
private items: Array<{ id: number, title: string, color: string }> = [
{ id: 1, title: '鸿蒙手机', color: '#4CAF50' },
{ id: 2, title: '鸿蒙平板', color: '#2196F3' },
{ id: 3, title: '鸿蒙手表', color: '#FF9800' },
];
build() {
Column() {
List() {
ForEach(this.items, (item: { id: number, title: string, color: string }) => {
ListItem() {
Row() {
// 共享元素:图片区域
// sharedTransition id 格式: "唯一标识"
Column()
.width(80)
.height(80)
.backgroundColor(item.color)
.borderRadius(8)
.sharedTransition(`image_${item.id}`, {
duration: 500,
curve: Curve.FastOutSlowIn,
delay: 0
})
Text(item.title)
.fontSize(18)
.margin({ left: 15 })
}
.padding(15)
.onClick(() => {
router.pushUrl({
url: 'pages/DetailTransitionPage',
params: { itemId: item.id, itemTitle: item.title, itemColor: item.color }
});
})
}
})
}
}
.width('100%')
.height('100%')
}
}
// ====== 页面B:详情页 ======
@Entry
@Component
struct DetailTransitionPage {
@State itemId: number = 0;
@State itemTitle: string = '';
@State itemColor: string = '';
aboutToAppear() {
const params = router.getParams() as Record<string, Object>;
this.itemId = params.itemId as number;
this.itemTitle = params.itemTitle as string;
this.itemColor = params.itemColor as string;
}
build() {
Column() {
// 共享元素:与列表页的图片区域共享
Column()
.width('100%')
.height(300)
.backgroundColor(this.itemColor)
.sharedTransition(`image_${this.itemId}`, {
duration: 500,
curve: Curve.FastOutSlowIn,
delay: 0
})
Text(this.itemTitle)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20 })
// 页面详情内容...
Text('这里是商品详情内容...')
.fontSize(16)
.margin({ top: 10 })
}
.width('100%')
.height('100%')
}
// 页面转场动画
pageTransition(): void {
PageTransitionEnter({ duration: 500 })
.slide(SlideEffect.Right)
PageTransitionExit({ duration: 500 })
.slide(SlideEffect.Right)
}
}
7.4 Navigation 自定义转场动画
Navigation 组件支持通过 navDestination 的动画属性自定义转场:
@Entry
@Component
struct NavTransitionDemo {
navPathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.navPathStack) {
Column() {
Text('首页')
.fontSize(24)
Button('跳转到详情(自定义动画)')
.onClick(() => {
this.navPathStack.pushPath({
name: 'DetailPage',
param: {},
// 指定此页面的进入动画
onShown: undefined
});
})
}
}
.mode(NavigationMode.Stack)
.navDestination(this.buildNavDestination)
// Navigation 级别的自定义动画
.customNavTransitionTransition((from: NavContentInfo, to: NavContentInfo, operation: NavigationOperation) => {
// operation: PUSH / POP / REPLACE
const progress = 0; // 动画进度 0~1,由系统驱动
// 返回自定义动画控制器
return undefined; // 返回 undefined 使用默认动画
})
}
@Builder
buildNavDestination(name: string, param: Object) {
if (name === 'DetailPage') {
NavDestination() {
Column() {
Text('详情页')
}
}
.title('详情页')
// NavDestination 级别的自定义转场
.transitions([
TransitionEffect.SLIDE(SlideEffect.Right)
.combine(TransitionEffect.OPACITY)
.animation({ duration: 500, curve: Curve.EaseInOut })
])
}
}
}
八、深度链接
8.1 URI Scheme 跳转
深度链接允许通过 URI 从外部(其他应用、浏览器、通知等)直达应用内特定页面。
// ====== 1. 在 module.json5 中配置 URI Scheme ======
// entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
],
// 配置深度链接 URI
"uris": [
{
"scheme": "myapp", // URI scheme
"host": "detail", // 主机名
"path": "/product" // 路径
},
{
"scheme": "myapp",
"host": "profile",
"path": "/user"
},
{
"scheme": "https", // 支持 https scheme
"host": "www.myapp.com",
"path": "/order"
}
]
}
]
}
}
8.2 Ability 中处理深度链接
// EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import router from '@ohos.router';
export default class EntryAbility extends UIAbility {
// 处理深度链接入口
onNewWant(want, launchParam) {
this.handleDeepLink(want);
}
onCreate(want, launchParam) {
this.handleDeepLink(want);
}
// 解析并处理深度链接
private handleDeepLink(want) {
if (!want || !want.uri) {
return;
}
const uri = want.uri;
console.info(`深度链接 URI: ${uri}`);
// 解析 URI:myapp://detail/product?id=42
try {
const urlObj = new URL(uri);
const scheme = urlObj.protocol.replace(':', ''); // myapp
const host = urlObj.hostname; // detail
const path = urlObj.pathname; // /product
const searchParams = urlObj.searchParams; // id=42
// 根据路径路由到不同页面
switch (`${host}${path}`) {
case 'detail/product': {
const productId = searchParams.get('id') ?? '0';
router.pushUrl({
url: 'pages/DetailPage',
params: { productId: parseInt(productId), source: 'deep_link' }
});
break;
}
case 'profile/user': {
const userId = searchParams.get('uid') ?? '0';
router.pushUrl({
url: 'pages/ProfilePage',
params: { userId: parseInt(userId), source: 'deep_link' }
});
break;
}
case 'www.myapp.com/order': {
const orderId = searchParams.get('orderId') ?? '';
router.pushUrl({
url: 'pages/OrderDetailPage',
params: { orderId, source: 'deep_link' }
});
break;
}
default:
console.warn(`未知的深度链接路径: ${host}${path}`);
router.pushUrl({ url: 'pages/Index' });
}
} catch (e) {
console.error(`深度链接解析失败: ${e}`);
}
}
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error(`加载页面失败: ${JSON.stringify(err)}`);
return;
}
});
}
}
8.3 App Linking(华为深度链接服务)
// App Linking 是华为提供的跨平台深度链接服务
// 支持跨设备、跨应用的页面直达
import linking from '@ohos.app.ability.link';
// 创建 App Linking 链接
async function createAppLinking(productId: string): Promise<string> {
// 实际使用需要集成 App Linking SDK
// 此处展示概念流程
const deepLink = `myapp://detail/product?id=${productId}`;
return deepLink;
}
// 在另一个应用中打开 App Linking
async function openAppLinking(uri: string) {
// 通过 Context 启动目标 Ability
// 系统会根据 URI 匹配到注册了该 scheme 的应用
}
九、页面生命周期
9.1 @Entry 页面生命周期
@Entry
@Component
struct LifecycleDemo {
@State message: string = '生命周期演示';
// ====== 页面生命周期回调 ======
// 页面即将构建 CustomDialog 之前调用
aboutToAppear() {
console.info('[Lifecycle] aboutToAppear — 页面即将显示');
// 适合做:数据初始化、参数获取、网络请求发起
}
// 页面完全销毁后调用
aboutToDisappear() {
console.info('[Lifecycle] aboutToDisappear — 页面即将销毁');
// 适合做:资源释放、定时器清除、监听器移除
}
// 页面每次显示时触发(包括从其他页面返回时)
onPageShow() {
console.info('[Lifecycle] onPageShow — 页面可见');
// 适合做:刷新数据、恢复动画、检查登录态
}
// 页面每次隐藏时触发
onPageHide() {
console.info('[Lifecycle] onPageHide — 页面隐藏');
// 适合做:暂停动画、保存临时状态
}
// 按返回键时触发
onBackPress(): boolean {
console.info('[Lifecycle] onBackPress — 用户按返回键');
// 返回 true 表示自行处理返回逻辑,不再执行默认返回
// 返回 false 或不返回表示执行默认返回
if (this.hasUnsavedChanges()) {
// 有未保存的数据,弹窗确认
this.showExitConfirmDialog();
return true; // 拦截返回
}
return false; // 允许默认返回
}
private hasUnsavedChanges(): boolean {
// 检查是否有未保存的修改
return true;
}
private showExitConfirmDialog() {
// 显示确认对话框
console.info('确认退出?');
}
build() {
Column() {
Text(this.message)
.fontSize(24)
}
.width('100%')
.height('100%')
}
}
页面生命周期流程:
┌─────────────────────────────────────────────────────────────────┐
│ @Entry 页面生命周期流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 页面创建 │
│ │ │
│ ▼ │
│ aboutToAppear() ──── 初始化数据、获取路由参数 │
│ │ │
│ ▼ │
│ build() ─────────── 构建页面 UI │
│ │ │
│ ▼ │
│ onPageShow() ────── 页面可见(首次 + 每次返回时触发) │
│ │ │
│ │ ◄──── 用户交互 │
│ │ │
│ │ pushUrl 跳转到其他页面 │
│ ▼ │
│ onPageHide() ────── 页面隐藏 │
│ │ │
│ │ ◄──── router.back() 返回此页面 │
│ │ │
│ ▼ │
│ onPageShow() ────── 再次可见 │
│ │ │
│ │ replaceUrl 替换 或 连续 back 导致此页出栈 │
│ ▼ │
│ onPageHide() ────── 页面隐藏 │
│ │ │
│ ▼ │
│ aboutToDisappear() ── 页面销毁,释放资源 │
│ │
│ 特殊:onBackPress() ─ 用户按返回键时触发 │
│ 返回 true 拦截,返回 false 使用默认行为 │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 NavDestination 生命周期
@Component
struct NavDestLifecycleDemo {
navPathStack: NavPathStack = new NavPathStack();
build() {
NavDestination() {
Column() {
Text('NavDestination 生命周期演示')
.fontSize(24)
}
}
.title('生命周期')
// NavDestination 特有的生命周期回调
.onShown(() => {
console.info('[NavDest] onShown — 页面可见');
// 类似 onPageShow
})
.onHidden(() => {
console.info('[NavDest] onHidden — 页面隐藏');
// 类似 onPageHide
})
.onWillAppear(() => {
console.info('[NavDest] onWillAppear — 页面即将显示');
// 在 onShown 之前触发
})
.onWillShow(() => {
console.info('[NavDest] onWillShow — 页面将要显示');
})
.onWillDisappear(() => {
console.info('[NavDest] onWillDisappear — 页面即将隐藏');
})
.onWillHide(() => {
console.info('[NavDest] onWillHide — 页面将要隐藏');
})
.onBackPressed(() => {
console.info('[NavDest] onBackPressed — 用户按返回');
// 返回 true 拦截返回,返回 false 使用默认行为
return false;
})
}
}
9.3 生命周期实战:数据加载与刷新
@Entry
@Component
struct ProductListPage {
@State products: Product[] = [];
@State isLoading: boolean = false;
@State isRefreshing: boolean = false;
private lastLoadTime: number = 0;
private readonly REFRESH_INTERVAL = 30_000; // 30秒自动刷新阈值
aboutToAppear() {
// 首次加载数据
this.loadProducts();
}
onPageShow() {
// 每次页面可见时检查是否需要刷新
const now = Date.now();
if (now - this.lastLoadTime > this.REFRESH_INTERVAL) {
this.silentRefresh();
}
}
onPageHide() {
// 页面隐藏时暂停动画等操作
this.pauseAnimations();
}
aboutToDisappear() {
// 页面销毁时清理资源
this.cleanup();
}
// 首次加载(显示 loading)
private async loadProducts() {
this.isLoading = true;
try {
this.products = await this.fetchProducts();
this.lastLoadTime = Date.now();
} catch (error) {
console.error(`加载商品失败: ${error}`);
} finally {
this.isLoading = false;
}
}
// 静默刷新(不显示 loading)
private async silentRefresh() {
try {
this.products = await this.fetchProducts();
this.lastLoadTime = Date.now();
} catch (error) {
console.error(`刷新失败: ${error}`);
}
}
private async fetchProducts(): Promise<Product[]> {
// 模拟网络请求
return [
{ id: 1, name: '鸿蒙手机', price: 3999 },
{ id: 2, name: '鸿蒙平板', price: 2999 },
];
}
private pauseAnimations() { /* 暂停动画 */ }
private cleanup() { /* 清理资源 */ }
build() {
Column() {
if (this.isLoading) {
LoadingProgress()
.width(48)
.height(48)
.color('#e4393c')
} else {
List() {
ForEach(this.products, (product: Product) => {
ListItem() {
Text(`${product.name} - ¥${product.price}`)
.fontSize(16)
.padding(15)
}
})
}
}
}
.width('100%')
.height('100%')
}
}
interface Product {
id: number;
name: string;
price: number;
}
十、多 Ability 导航
鸿蒙应用可以包含多个 Ability,每个 Ability 拥有独立的页面栈,Ability 之间的导航通过 Context 实现。
10.1 多 Ability 架构
┌─────────────────────────────────────────────────────────────────┐
│ 多 Ability 导航架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 应用 (App) │ │
│ │ │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌────────────┐ │ │
│ │ │ MainAbility │ │ PayAbility │ │ WebAbility │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ 页面栈: │ │ 页面栈: │ │ 页面栈: │ │ │
│ │ │ [Home] │ │ [PayConfirm] │ │ [WebView] │ │ │
│ │ │ [Detail] │ │ [PayResult] │ │ │ │ │
│ │ │ [Order] │ │ │ │ │ │ │
│ │ └───────┬───────┘ └───────┬───────┘ └─────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └──────────────────┴─────────────────┘ │ │
│ │ Context 启动 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 优势: │
│ · 独立页面栈,互不影响 │
│ · 独立生命周期,按需加载 │
│ · 支持多窗口 │
│ · 适合功能模块解耦 │
│ │
└─────────────────────────────────────────────────────────────────┘
10.2 跨 Ability 导航实现
// ====== MainAbility 中跳转到 PayAbility ======
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
@Entry
@Component
struct OrderConfirmPage {
private context = getContext(this) as common.UIAbilityContext;
build() {
Column() {
Text('确认订单')
.fontSize(24)
Button('去支付(启动 PayAbility)')
.onClick(() => {
this.launchPayAbility();
})
}
}
private launchPayAbility() {
// 构建 Want 对象,指定目标 Ability
const want: Want = {
bundleName: 'com.example.myapp', // 应用包名
abilityName: 'PayAbility', // 目标 Ability 名称
parameters: { // 传递参数
orderId: 'ORD-20260511-001',
amount: 99.9,
currency: 'CNY'
}
};
// 启动目标 Ability
this.context.startAbility(want)
.then(() => {
console.info('PayAbility 启动成功');
})
.catch((error: Error) => {
console.error(`PayAbility 启动失败: ${error.message}`);
});
}
}
// ====== PayAbility 中接收参数并跳转页面 ======
// PayAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
export default class PayAbility extends UIAbility {
onCreate(want, launchParam) {
// 获取启动参数
const orderId = want.parameters?.orderId as string;
const amount = want.parameters?.amount as number;
console.info(`PayAbility 收到支付请求: orderId=${orderId}, amount=${amount}`);
// 将参数存入 AppStorage,供页面读取
AppStorage.setOrCreate('payOrderId', orderId);
AppStorage.setOrCreate('payAmount', amount);
}
onWindowStageCreate(windowStage: window.WindowStage) {
// 加载支付页面
windowStage.loadContent('pages/PayConfirmPage', (err, data) => {
if (err.code) {
console.error(`加载支付页面失败: ${JSON.stringify(err)}`);
}
});
}
// 支付完成后返回结果给 MainAbility
onNewWant(want, launchParam) {
// 处理新的启动请求(PayAbility 已在后台时)
const newOrderId = want.parameters?.orderId as string;
AppStorage.setOrCreate('payOrderId', newOrderId);
}
}
10.3 使用 startAbilityForResult 获取返回结果
// ====== 发送方:启动 Ability 并等待结果 ======
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
@Entry
@Component
struct OrderPage {
private context = getContext(this) as common.UIAbilityContext;
@State payResult: string = '未支付';
build() {
Column() {
Text(`支付状态: ${this.payResult}`)
Button('发起支付')
.onClick(() => {
this.requestPay();
})
}
}
private async requestPay() {
const want: Want = {
bundleName: 'com.example.myapp',
abilityName: 'PayAbility',
parameters: {
orderId: 'ORD-001',
amount: 199.9
}
};
try {
// startAbilityForResult 会等待目标 Ability 返回结果
const result = await this.context.startAbilityForResult(want, 0);
console.info(`支付结果: resultCode=${result.resultCode}`);
if (result.resultCode === 0) {
// 支付成功
const payStatus = result.want?.parameters?.status as string;
this.payResult = `支付成功: ${payStatus}`;
} else {
// 支付取消或失败
this.payResult = '支付取消';
}
} catch (error) {
this.payResult = '支付失败';
}
}
}
// ====== PayAbility 中返回结果 ======
// 在支付完成后调用 terminateSelfWithResult
export default class PayAbility extends UIAbility {
private context: common.UIAbilityContext = this.context;
// 支付成功时
paySuccess() {
this.context.terminateSelfWithResult({
resultCode: 0,
want: {
parameters: {
status: 'SUCCESS',
transactionId: 'TXN-20260511-001'
}
}
});
}
// 支付取消时
payCancel() {
this.context.terminateSelfWithResult({
resultCode: -1,
want: {
parameters: {
status: 'CANCELLED'
}
}
});
}
}
十一、与 Flutter / React Navigation / Vue Router 的对比
11.1 架构理念对比
| 维度 | 鸿蒙 Router | 鸿蒙 Navigation | Flutter Navigator | React Navigation | Vue Router |
|---|---|---|---|---|---|
| 核心理念 | 过程式路由 API | 声明式导航容器 | 命令式栈管理 | 声明式 Screen 管道 | 声明式路由配置 |
| 页面栈 | 系统管理 | NavPathStack 自管理 | Navigator 管理栈 | StackActions | history 栈 |
| 路由配置 | main_pages.json | @Builder 动态构建 | routes 数组 | Screen 组件声明 | 路由表配置 |
| 路由守卫 | 手动封装 | onRouteChange | RouteObserver | beforeNavigate | beforeEach |
| 深度链接 | URI Scheme 配置 | 同左 | onGenerateRoute | linking 配置 | history mode |
| 转场动画 | PageTransition | NavDestination 动画 | PageTransitions | transitionSpec | transition 组件 |
| Tab 导航 | Tabs 组件 | Tabs 组件 | TabBar + TabView | Tab Navigator | vue-router 无内置 |
| 返回拦截 | onBackPress | onBackPressed | WillPopScope | BackHandler | beforeRouteLeave |
| 传参方式 | params | param + onPop | arguments | params/state | query/params |
| 状态持久化 | AppStorage | NavPathStack | 状态管理方案 | 状态管理方案 | Pinia/Vuex |
| 学习曲线 | 低 | 中 | 中 | 中 | 低 |
11.2 路由跳转语法对比
// ====== 鸿蒙 Router ======
import router from '@ohos.router';
router.pushUrl({ url: 'pages/Detail', params: { id: 1 } });
// ====== 鸿蒙 Navigation ======
this.navPathStack.pushPath({ name: 'DetailPage', param: { id: 1 } });
// ====== Flutter Navigator ======
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(id: 1),
),
);
// ====== React Navigation ======
navigation.navigate('Detail', { id: 1 });
// ====== Vue Router ======
router.push({ name: 'Detail', params: { id: 1 } });
// 或 router.push('/detail/1')
11.3 路由守卫对比
// ====== 鸿蒙 Navigation 路由拦截 ======
// 在 navigateTo 方法中统一判断
navigateTo(name: string) {
if (this.isAuthRequired(name) && !this.isLoggedIn) {
this.navPathStack.pushPath({ name: 'LoginPage' });
return;
}
this.navPathStack.pushPath({ name });
}
// ====== Flutter 路由守卫 ======
// 通过 RouteObserver + RouteAware 实现
class AuthObserver extends RouteObserver<PageRoute> {
void didPush(Route route, Route? previousRoute) {
if (requiresAuth(route) && !isLoggedIn) {
navigator?.push(LoginPage());
}
}
}
// ====== React Navigation 路由守卫 ======
// 通过 Navigation Container + 状态判断实现
function App() {
return (
<NavigationContainer>
{isLoggedIn ? <AppStack /> : <AuthStack />}
</NavigationContainer>
);
}
// ====== Vue Router 路由守卫 ======
// 原生支持 beforeEach / beforeResolve / afterEach
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login');
} else {
next();
}
});
11.4 页面栈操作对比
| 操作 | 鸿蒙 Router | 鸿蒙 NavPathStack | Flutter | React Navigation | Vue Router |
|---|---|---|---|---|---|
| 压栈跳转 | pushUrl | pushPath | push | navigate | push |
| 替换跳转 | replaceUrl | replacePath | pushReplacement | replace | replace |
| 返回 | back | pop | pop | goBack | go(-1) |
| 返回指定页 | back(url) | popToName | popUntil | reset | go(n) |
| 清空栈 | clear | clear | popToTop | reset | — |
| 获取栈大小 | getLength | size | canPop | — | history.length |
| 移除中间页 | — | removeByIndexes | — | — | — |
11.5 生态与工具链对比
| 维度 | 鸿蒙 | Flutter | React Native | Vue |
|---|---|---|---|---|
| 调试工具 | DevEco Studio | DevTools | Flipper | Vue DevTools |
| 路由生成 | 手动配置 | go_router / auto_route | — | unplugin-vue-router |
| 类型安全 | ArkTS 强类型 | Dart 强类型 | TypeScript | TypeScript |
| 代码分割 | 多 Ability | — | — | 懒加载路由 |
| DevTools | HiProfiler | Flutter Inspector | React DevTools | Vue DevTools |
| 官方推荐 | Navigation | Navigator 2.0 | React Navigation | Vue Router |
十二、综合实战:电商应用导航架构
将前面所有知识整合为一个完整的电商应用导航架构:
// ====== 电商应用主入口 ======
@Entry
@Component
struct ECommerceApp {
@State currentIndex: number = 0;
@StorageLink('isLoggedIn') isLoggedIn: boolean = false;
// 每个 Tab 的独立导航栈
homeNavStack: NavPathStack = new NavPathStack();
categoryNavStack: NavPathStack = new NavPathStack();
cartNavStack: NavPathStack = new NavPathStack();
profileNavStack: NavPathStack = new NavPathStack();
// 需要鉴权的页面
private authPages: string[] = ['OrderListPage', 'AddressPage', 'CouponPage'];
// 安全导航方法
navigateInTab(tabIndex: number, pageName: string, param?: Object) {
const stack = this.getNavStack(tabIndex);
if (this.authPages.includes(pageName) && !this.isLoggedIn) {
stack.pushPath({
name: 'LoginPage',
param: { redirectTab: tabIndex, redirectPage: pageName, redirectParam: param }
});
return;
}
stack.pushPath({ name: pageName, param });
}
private getNavStack(tabIndex: number): NavPathStack {
switch (tabIndex) {
case 0: return this.homeNavStack;
case 1: return this.categoryNavStack;
case 2: return this.cartNavStack;
case 3: return this.profileNavStack;
default: return this.homeNavStack;
}
}
build() {
Column() {
Tabs({ index: $$this.currentIndex }) {
// ====== Tab 0:首页 ======
TabContent() {
Navigation(this.homeNavStack) {
HomeTab({ navStack: this.homeNavStack, navigateInTab: (name, param?) => {
this.navigateInTab(0, name, param);
}})
}
.mode(NavigationMode.Stack)
.navDestination(this.homeNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(0, '首页'))
// ====== Tab 1:分类 ======
TabContent() {
Navigation(this.categoryNavStack) {
CategoryTab({ navStack: this.categoryNavStack })
}
.mode(NavigationMode.Stack)
.navDestination(this.categoryNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(1, '分类'))
// ====== Tab 2:购物车 ======
TabContent() {
Navigation(this.cartNavStack) {
CartTab({ navStack: this.cartNavStack, navigateInTab: (name, param?) => {
this.navigateInTab(2, name, param);
}})
}
.mode(NavigationMode.Stack)
.navDestination(this.cartNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(2, '购物车'))
// ====== Tab 3:我的 ======
TabContent() {
Navigation(this.profileNavStack) {
ProfileTab({ navStack: this.profileNavStack, navigateInTab: (name, param?) => {
this.navigateInTab(3, name, param);
}})
}
.mode(NavigationMode.Stack)
.navDestination(this.profileNavDest)
.hideTitleBar(true)
}
.tabBar(this.buildTabBar(3, '我的'))
}
.vertical(false)
.scrollable(false)
.barMode(BarMode.Fixed)
.barPosition(BarPosition.End)
.onChange((index: number) => {
this.currentIndex = index;
})
}
.width('100%')
.height('100%')
}
// 各 Tab 的 NavDestination 构建器
@Builder
homeNavDest(name: string, param: Object) {
if (name === 'ProductDetailPage') {
ProductDetailPage({ navPathStack: this.homeNavStack, param })
} else if (name === 'SearchPage') {
SearchPage({ navPathStack: this.homeNavStack })
} else if (name === 'LoginPage') {
LoginPage({ navPathStack: this.homeNavStack, param })
}
}
@Builder
categoryNavDest(name: string, param: Object) {
if (name === 'SubCategoryPage') {
SubCategoryPage({ navPathStack: this.categoryNavStack, param })
}
}
@Builder
cartNavDest(name: string, param: Object) {
if (name === 'OrderConfirmPage') {
OrderConfirmPage({ navPathStack: this.cartNavStack, param })
} else if (name === 'LoginPage') {
LoginPage({ navPathStack: this.cartNavStack, param })
}
}
@Builder
profileNavDest(name: string, param: Object) {
if (name === 'OrderListPage') {
OrderListPage({ navPathStack: this.profileNavStack, param })
} else if (name === 'AddressPage') {
AddressPage({ navPathStack: this.profileNavStack })
} else if (name === 'LoginPage') {
LoginPage({ navPathStack: this.profileNavStack, param })
}
}
@Builder
buildTabBar(index: number, title: string) {
Column() {
Text(title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#e4393c' : '#999999')
}
}
}
// ====== 首页 Tab 内容 ======
@Component
struct HomeTab {
navStack: NavPathStack = new NavPathStack();
navigateInTab: (name: string, param?: Object) => void = () => {};
@State banners: string[] = ['banner1', 'banner2', 'banner3'];
build() {
Column() {
// 搜索栏
Row() {
TextInput({ placeholder: '搜索商品' })
.layoutWeight(1)
.onClick(() => {
this.navStack.pushPath({ name: 'SearchPage' });
})
}
.padding({ left: 15, right: 15 })
// 轮播图
Swiper() {
ForEach(this.banners, (banner: string) => {
Text(banner)
.width('100%')
.height(180)
.backgroundColor('#ddd')
.textAlign(TextAlign.Center)
})
}
.autoPlay(true)
.interval(3000)
.margin({ top: 10 })
// 商品列表
List() {
ForEach([1, 2, 3, 4, 5], (id: number) => {
ListItem() {
Row() {
Text(`商品 ${id}`)
.fontSize(16)
}
.padding(15)
.onClick(() => {
this.navStack.pushPath({
name: 'ProductDetailPage',
param: { productId: id }
});
})
}
})
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
}
电商应用导航全景:
┌─────────────────────────────────────────────────────────────────────┐
│ 电商应用导航全景图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Tab: 首页 Tab: 分类 Tab: 购物车 │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ HomeTab │ │ CategoryTab│ │ CartTab │ │
│ │ │ │ │ │ │ │
│ │ SearchBar │ │ 一级分类 │ │ 购物车列表 │ │
│ │ └─Search │ │ └─二级分类 │ │ └─确认订单 │ │
│ │ Banner │ │ │ │ (需登录)│ │
│ │ ProductList│ │ │ │ │ │
│ │ └─Detail │ │ │ │ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ Tab: 我的 │
│ ┌────────────┐ │
│ │ ProfileTab │ │
│ │ │ │
│ │ 订单列表 │ ← 需登录 │
│ │ 地址管理 │ ← 需登录 │
│ │ 优惠券 │ ← 需登录 │
│ │ 设置 │ │
│ └────────────┘ │
│ │
│ ┌────────────┐ │
│ │ LoginPage │ ← 统一登录页,登录后重定向回原目标 │
│ └────────────┘ │
│ │
│ 深度链接入口: │
│ myapp://detail/product?id=42 → 首页 Tab → ProductDetailPage │
│ myapp://profile/user?uid=1 → 我的 Tab → ProfilePage │
│ │
└─────────────────────────────────────────────────────────────────────┘
面试题
1. Router 模块的 pushUrl 和 replaceUrl 有什么区别?分别在什么场景下使用?
答案:
pushUrl 和 replaceUrl 的核心区别在于对页面栈的操作方式不同:
- pushUrl:将目标页面压入页面栈顶部,当前页面保留在栈中。用户可以通过返回键回到原页面。栈深度 +1。
- replaceUrl:用目标页面替换当前栈顶页面,当前页面被销毁出栈。用户无法通过返回键回到原页面。栈深度不变。
使用场景:
pushUrl适用于常规的层级跳转,如首页 -> 列表 -> 详情,用户需要返回上一级。replaceUrl适用于”不可逆”的跳转,典型场景包括:- 登录页跳转到主页后,不希望用户返回到登录页
- 引导页完成后跳转到首页
- 支付完成后跳转到支付结果页,不希望返回到支付确认页
- 页面栈接近上限(32层)时,用 replace 代替 push 防止栈溢出
// 登录后替换,不可返回
router.replaceUrl({ url: 'pages/MainPage' });
// 正常跳转,可返回
router.pushUrl({ url: 'pages/DetailPage', params: { id: 1 } });
2. Navigation 组件相比 Router 模块有哪些优势?鸿蒙为什么推荐使用 Navigation?
答案:
Navigation 组件相比 Router 模块有以下核心优势:
-
声明式导航管理:Navigation 基于 ArkUI 声明式范式,NavDestination 通过 @Builder 动态构建,与 UI 代码更内聚,类型更安全。
-
自主路由栈管理:NavPathStack 由开发者自行管理,提供了比 Router 更丰富的栈操作 API(如 removeByIndexes、removeByName、popToName),可以精确控制栈中任意位置的页面。
-
原生路由拦截能力:Navigation 支持 onRouteChange 回调,可以在路由变化时进行拦截,实现路由守卫、鉴权拦截等逻辑,而 Router 模块没有原生的拦截能力。
-
灵活的转场动画:NavDestination 支持更丰富的自定义转场动画,包括 transitions 属性和 customNavTransitionTransition,远超 Router 的 PageTransition。
-
同 Ability 内多级导航:Navigation 在同一个 Ability 内管理多级页面,避免了多 Ability 的启动开销,性能更优。
-
onPop 回调机制:pushPath 支持 onPop 回调,天然支持页面间回传参数,比 Router 的 getParams + onPageShow 方案更优雅。
-
适配多设备:Navigation 支持 Stack/Split 两种模式,同一套代码可适配手机(Stack)和平板(Split),而 Router 只支持单页面展示。
-
NavDestination 生命周期更精细:提供 onWillAppear/onWillShow/onShown/onWillDisappear/onWillHide/onHidden 六个回调,比 @Entry 页面的生命周期控制更精确。
鸿蒙推荐 Navigation 的原因:它是面向未来的一站式导航方案,更契合 ArkUI 声明式开发范式,能力更完整,扩展性更强。Router 模块主要保留用于兼容旧项目。
3. 如何实现路由鉴权拦截?未登录时跳转到登录页,登录后自动回到原目标页面?
答案:
路由鉴权拦截可以通过两种方式实现:
方式一:Navigation 的封装拦截(推荐)
// 1. 维护需要鉴权的页面列表
private authPages: string[] = ['ProfilePage', 'OrderPage', 'SettingsPage'];
// 2. 封装安全导航方法
navigateTo(name: string, param?: Object) {
if (this.authPages.includes(name) && !this.isLoggedIn) {
// 未登录:跳转到登录页,携带重定向信息
this.navPathStack.pushPath({
name: 'LoginPage',
param: {
redirectPage: name, // 记录原目标
redirectParam: param // 记录原参数
}
});
return;
}
this.navPathStack.pushPath({ name, param });
}
// 3. 登录页完成登录后,读取重定向信息并跳转
private doLogin() {
const p = this.param as Record<string, Object>;
const redirectPage = p?.redirectPage as string;
if (redirectPage) {
// 有重定向目标:替换登录页,跳转到原目标
this.navPathStack.replacePath({
name: redirectPage,
param: p?.redirectParam
});
} else {
this.navPathStack.pop();
}
}
方式二:Router 手动守卫封装
// 注册全局前置守卫
beforeEach((to, params) => {
const requiresAuth = routeGuardRegistry.get(to)?.requiresAuth;
if (requiresAuth && !AppStorage.get('isLoggedIn')) {
return 'pages/LoginPage'; // 重定向
}
return true; // 放行
});
// 使用守卫跳转
guardedPush('pages/ProfilePage');
关键要点:
- 重定向信息(redirectPage/redirectParam)必须完整传递,确保登录后能回到原目标
- 登录成功后使用 replacePath 而非 pushPath,避免登录页残留在栈中
- 登录状态应使用 AppStorage 全局管理,确保所有组件可访问
4. NavPathStack 的常用操作有哪些?如何实现”返回到指定页面”?
答案:
NavPathStack 提供了丰富的栈操作 API:
| 操作 | API | 说明 |
|---|---|---|
| 压栈跳转 | pushPath / pushPathByName | 新页面入栈 |
| 替换栈顶 | replacePath / replacePathByName | 替换当前页面 |
| 出栈返回 | pop | 弹出栈顶 |
| 返回到指定 name | popToName | 弹出到指定页面 |
| 返回到指定索引 | popToIndex | 弹出到指定位置 |
| 清空栈 | clear | 清除所有历史 |
| 移除指定页面 | removeByIndexes / removeByName | 不触发转场动画 |
| 获取栈信息 | getAllPathName / size / isEmpty | 查询栈状态 |
| 获取参数 | getParamByName / getParamByIndex | 获取指定页面参数 |
| 获取父页面 | getParent | 获取上一级页面信息 |
“返回到指定页面”的实现:
// 方式1:popToName — 返回到指定 name 的页面
// 假设栈:[Home, List, Detail, Comment]
// 执行 popToName('List')
// 结果栈:[Home, List] — Detail 和 Comment 被弹出
this.navPathStack.popToName('ListPage');
// 方式2:popToIndex — 返回到指定索引的页面
// 假设栈:[Home(0), List(1), Detail(2), Comment(3)]
// 执行 popToIndex(1)
// 结果栈:[Home(0), List(1)]
this.navPathStack.popToIndex(1);
// 方式3:clear + push — 清空栈后重新开始
this.navPathStack.clear();
this.navPathStack.pushPath({ name: 'HomePage' });
// 方式4:removeByIndexes — 精确移除中间页面
// 假设栈:[Home(0), List(1), Detail(2), Comment(3)]
// 移除中间的 Detail
this.navPathStack.removeByIndexes([2]);
// 结果栈:[Home, List, Comment]
popToName 是最常用的”返回到指定页面”方式,它会弹出目标页面之上的所有页面,且触发转场动画。removeByIndexes 则是不触发动画的静默移除,适合在后台清理不需要的页面。
5. 页面栈最大支持多少层?如何避免页面栈溢出?
答案:
鸿蒙页面栈默认最大支持 32 层。当页面栈达到上限后,再次调用 pushUrl 会抛出错误码 100003。
避免页面栈溢出的策略:
-
使用 replaceUrl 替代 pushUrl:在不需要返回的场景下使用 replace,如登录后跳转主页。
-
使用 Single 路由模式:当目标页面已在栈中时,Single 模式会回退到该页面而不是新建实例。
-
封装安全跳转方法:在跳转前检查栈深度,接近上限时自动切换策略。
function safePush(url: string, params?: Object) {
const MAX_STACK = 30;
if (router.getLength() >= MAX_STACK) {
router.replaceUrl({ url, params });
} else {
router.pushUrl({ url, params });
}
}
-
及时清理无用页面:在合适时机调用 router.clear() 或 NavPathStack 的 removeByIndexes 清理中间页面。
-
合理设计导航层级:避免过深的页面跳转链路,通常 3-5 层已能满足绝大多数场景。
-
使用 Navigation 替代 Router:NavPathStack 在同一 Ability 内管理页面,不占用系统页面栈配额,更灵活。
6. SharedTransition 共享元素转场的实现原理和使用方式是什么?
答案:
原理: SharedTransition(共享元素转场)通过在两个页面中为具有相同 sharedTransition id 的组件建立映射关系,在页面切换时自动计算两个组件的位置、大小差异,并执行平滑的过渡动画,使元素看起来像从原位置”飞”到新位置。这类似于 Android 的 SharedElement Transition 和 Flutter 的 Hero 动画。
使用方式:
// 页面A:列表页
Column()
.width(80)
.height(80)
.backgroundColor('#4CAF50')
.sharedTransition('shared_image_1', { // 设置共享元素 id
duration: 500,
curve: Curve.FastOutSlowIn,
delay: 0
})
// 页面B:详情页(同一个 id)
Column()
.width('100%')
.height(300)
.backgroundColor('#4CAF50')
.sharedTransition('shared_image_1', { // 相同的 id
duration: 500,
curve: Curve.FastOutSlowIn,
delay: 0
})
关键要点:
- 两个页面的共享元素必须使用相同的 id,id 格式为字符串,建议使用有业务含义的命名如
image_${itemId}。 - 共享元素的组件类型不必相同(如列表页用 Image,详情页可以用 Column),但视觉上建议有对应关系。
SharedTransition的配置(duration/curve/delay)在两个页面可以不同,系统会取两者的合理值。- 共享元素转场与 PageTransition 可以同时使用,两者不冲突。
- 如果使用 Navigation,共享元素需要在 NavDestination 中配置。
- 多个共享元素可以同时参与转场,各自使用不同的 id。
7. Tabs + Navigation 组合架构中,如何处理返回键与 Tab 切换的关系?
答案:
在 Tabs + Navigation 组合架构中,返回键的处理需要遵循”先栈后 Tab”的优先级:
处理逻辑:
- 优先处理当前 Tab 的 Navigation 栈:如果当前 Tab 的 NavPathStack 中有二级页面,返回键应先 pop 当前 Tab 的栈。
- 栈为空时再考虑 Tab 切换:如果当前 Tab 的 NavPathStack 只有首页,可以根据业务需求决定是切换到上一个 Tab 还是退出应用。
@Entry
@Component
struct TabNavApp {
@State currentIndex: number = 0;
homeNavStack: NavPathStack = new NavPathStack();
categoryNavStack: NavPathStack = new NavPathStack();
cartNavStack: NavPathStack = new NavPathStack();
profileNavStack: NavPathStack = new NavPathStack();
// 获取当前 Tab 的 NavPathStack
private getCurrentStack(): NavPathStack {
switch (this.currentIndex) {
case 0: return this.homeNavStack;
case 1: return this.categoryNavStack;
case 2: return this.cartNavStack;
case 3: return this.profileNavStack;
default: return this.homeNavStack;
}
}
// 处理返回键
onBackPress(): boolean {
const currentStack = this.getCurrentStack();
if (currentStack.size() > 1) {
// 当前 Tab 有二级页面,pop 栈
currentStack.pop();
return true; // 拦截返回
}
if (this.currentIndex !== 0) {
// 当前在非首页 Tab,切回首页 Tab
this.currentIndex = 0;
return true; // 拦截返回
}
// 首页 Tab 且栈为空,允许默认退出
return false;
}
build() {
// ... Tabs + Navigation 组合 UI
}
}
注意事项:
- 每个 Tab 必须有独立的 NavPathStack,避免 Tab 切换时页面栈互相干扰。
- Tab 切换时各 Tab 的 NavPathStack 状态保持不变,用户切回 Tab 时能看到之前的页面层级。
- 某些场景需要全局清空栈:如退出登录后,应清除所有 Tab 的 NavPathStack 并切回首页。
8. 鸿蒙的深度链接如何实现?从其他应用跳转到应用内指定页面的完整流程是什么?
答案:
深度链接的实现分为配置、解析、路由三个阶段:
第一阶段:配置 URI Scheme
在 module.json5 中为目标 Ability 注册 URI Scheme:
{
"abilities": [{
"name": "EntryAbility",
"uris": [{
"scheme": "myapp",
"host": "detail",
"path": "/product"
}, {
"scheme": "https",
"host": "www.myapp.com",
"path": "/order"
}]
}]
}
第二阶段:Ability 中解析 URI
在 EntryAbility 的 onCreate 和 onNewWant 中解析 URI 并路由:
onCreate(want, launchParam) {
this.handleDeepLink(want);
}
onNewWant(want, launchParam) {
this.handleDeepLink(want); // 应用已在后台时通过此回调处理
}
private handleDeepLink(want) {
if (!want?.uri) return;
const uri = new URL(want.uri);
const host = uri.hostname;
const path = uri.pathname;
const params = uri.searchParams;
// 根据路径路由
if (host === 'detail' && path === '/product') {
const id = params.get('id');
router.pushUrl({ url: 'pages/DetailPage', params: { productId: id, source: 'deep_link' } });
}
}
第三阶段:完整流程
1. 其他应用/浏览器/通知发起跳转
↓
2. 系统根据 URI Scheme 匹配到目标 Ability
↓
3. 如果应用未运行 → 启动应用 → onCreate(want)
如果应用在后台 → 恢复应用 → onNewWant(want)
↓
4. 在 want.uri 中获取完整的 URI
↓
5. 解析 URI(scheme/host/path/params)
↓
6. 根据解析结果执行路由跳转
↓
7. 加载目标页面,传递深度链接参数
关键注意点:
onNewWant必须处理,否则应用在后台时收到深度链接无法响应。- URI 中的参数通过
searchParams获取,注意类型转换(都是 string)。 - 深度链接跳转前应先加载首页(windowStage.loadContent),再在首页 onPageShow 中根据参数跳转,避免页面栈异常。
- 支持
httpsscheme 可以实现 App Linking(通用链接),不需要用户选择打开方式。 - 深度链接参数应做校验,避免恶意参数导致应用崩溃。
相关链接
- ArkUI声明式开发 — ArkUI 声明式 UI 范式基础,Navigation/NavDestination/NavPathStack 均基于此
- 鸿蒙系统架构与开发入门 — 鸿蒙整体架构与 Ability 模型,多 Ability 导航的基础
- Flutter路由与导航 — Flutter Navigator/Route 体系对比参考
- Vue生态Router与Pinia — Vue Router 路由守卫/导航守卫对比参考