我就问:你的导航一团乱麻,用户凭什么不迷路?
导航不是“点点按钮就跳页”,而是一套可推理、可测试、可演进的架构契约。尤其在鸿蒙(HarmonyOS/OpenHarmony)Stage + ArkUI 场景里,“全局导航容器”与“局部导航容器”如同地图与罗盘:前者管应用级路由与页面栈,后者管模块内的子流程(模态/弹窗/子任务)。这篇把页面栈管理、参数传递、深链、模态内 Navigation、返回一致性、复杂场景解耦一股脑讲透,还给你一套可落地的
我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~
前言
先抛结论:导航不是“点点按钮就跳页”,而是一套可推理、可测试、可演进的架构契约。尤其在鸿蒙(HarmonyOS/OpenHarmony)Stage + ArkUI 场景里,“全局导航容器”与“局部导航容器”如同地图与罗盘:前者管应用级路由与页面栈,后者管模块内的子流程(模态/弹窗/子任务)。这篇把页面栈管理、参数传递、深链、模态内 Navigation、返回一致性、复杂场景解耦一股脑讲透,还给你一套可落地的代码骨架,拿去就能抄(然后再改得像你写的一样😉)。
0. 设计目标(把话说在前面)
- 一致的返回行为:任意入口、任意形态下,“返回”都能预期可解释。
- 全局/局部解耦:全局容器不感知业务细节;局部容器不破坏全局栈。
- 参数传递类型安全:入参/出参可校验;结果回传可
await。 - 深链(Deep Link)直达:URL → 路由 → 页面状态,一步到位;冷/暖启动一致。
- 模态/弹窗内可二次导航:不污染主栈;关闭时原子回滚。
- 可测试:无 UI 条件下重放导航脚本;栈与事件可观测。
1. 总体架构:全局 NavHost + 局部 NavContainer
┌──────────────── App (MainAbility) ────────────────┐
│ GlobalNavHost (App 级容器) │
│ ├─ Stack: [Splash] -> [TabsShell] -> [Detail] │
│ │ │
│ └─ DeepLinkDispatcher │
│ │
│ TabsShell (多栈外壳:Home / Explore / Profile) │
│ ├─ LocalNavContainer<Home> // 局部容器 │
│ │ Stack: [Home] -> [Filter] -> [Result] │
│ ├─ LocalNavContainer<Explore> │
│ └─ LocalNavContainer<Profile> │
│ │
│ ModalHost (模态域,独立栈) │
│ └─ Stack: [PhotoPicker] -> [Crop] -> [Confirm] │
└───────────────────────────────────────────────────┘
关键词:“多栈”(每个 Tab/模态都有自己的子栈)、“域”(全局域/模态域/局部域),以及**“单一出口”**(所有返回事件统一由策略层仲裁)。
2. 路由表与类型定义(强约束,弱耦合)
2.1 Route 声明:路径、入参与守卫
// /router/routes.ts
export type RouteName =
| 'splash' | 'tabs'
| 'detail' | 'login'
| 'home' | 'home.filter' | 'home.result'
| 'explore' | 'profile'
| 'modal.photo' | 'modal.crop' | 'modal.confirm';
export interface RouteConfig<TParams = any, TResult = any> {
path: string; // 逻辑路径,非 UI 文件路径
params?: (raw: unknown) => TParams; // 入参校验(zod/自写校验器)
canActivate?: () => Promise<boolean>; // 守卫(如已登录)
modal?: boolean; // 是否在模态域
returns?: boolean; // 是否有结果回传(presentForResult)
}
export const Routes: Record<RouteName, RouteConfig> = {
splash: { path: '/splash' },
tabs: { path: '/tabs' },
detail: { path: '/detail', params: (x:any)=>({ id: String(x?.id) }) },
home: { path: '/home' },
'home.filter': { path: '/home/filter', params: (x:any)=>({ q: String(x?.q||'') }) },
'home.result': { path: '/home/result', params: (x:any)=>({ q: String(x?.q||'') }) },
explore: { path: '/explore' },
profile: { path: '/profile', canActivate: async()=> /* isLogin() */ true },
'modal.photo': { path: '/modal/photo', modal: true },
'modal.crop': { path: '/modal/crop', modal: true },
'modal.confirm': { path: '/modal/confirm', modal: true, returns: true },
};
2.2 路由服务接口(全局唯一入口)
// /router/RouterService.ts
export type NavDomain = 'global' | 'modal' | 'home' | 'explore' | 'profile';
export interface RouterService {
push<T extends RouteName>(name: T, params?: any, domain?: NavDomain): Promise<void>;
replace<T extends RouteName>(name: T, params?: any, domain?: NavDomain): Promise<void>;
pop(domain?: NavDomain): Promise<boolean>; // 返回 true 表示成功弹栈
resetTo<T extends RouteName>(name: T, params?: any): Promise<void>;
present<T extends RouteName, R = any>(name: T, params?: any): Promise<R>; // 模态 + 结果
handleDeepLink(url: string): Promise<void>;
current(domain?: NavDomain): { name: RouteName; params: any } | null;
}
3. 页面栈管理:多容器多栈的“统一调度”
3.1 栈模型与可观测状态
// /router/NavState.ts
type StackItem = { name: RouteName; params?: any; key: string };
export class Stack {
private items: StackItem[] = [];
listeners = new Set<() => void>();
get top(): StackItem | undefined { return this.items[this.items.length - 1]; }
get size() { return this.items.length; }
snapshot() { return [...this.items]; }
push(item: StackItem) { this.items.push(item); this.emit(); }
replace(item: StackItem) { this.items.pop(); this.items.push(item); this.emit(); }
pop(): boolean { if (this.items.length <= 1) return false; this.items.pop(); this.emit(); return true; }
reset(item: StackItem) { this.items = [item]; this.emit(); }
onChange(cb: ()=>void) { this.listeners.add(cb); return ()=>this.listeners.delete(cb); }
private emit() { this.listeners.forEach(fn=>fn()); }
}
export class NavStore {
stacks: Record<NavDomain, Stack> = {
global: new Stack(),
modal: new Stack(),
home: new Stack(),
explore:new Stack(),
profile:new Stack(),
};
// 统一订阅暴露,便于测试/埋点
}
export const navStore = new NavStore();
3.2 Router 实现(核心动作)
// /router/RouterImpl.ts
import { Routes, RouteName } from './routes';
import { navStore, Stack } from './NavState';
function keyOf(name: RouteName) { return `${name}:${Date.now()}:${Math.random()}`; }
export const Router: RouterService = {
async push(name, params, domain='global') {
const cfg = Routes[name];
await guard(cfg);
const parsed = cfg.params ? cfg.params(params) : params;
navStore.stacks[domain].push({ name, params: parsed, key: keyOf(name) });
renderHost(domain);
},
async replace(name, params, domain='global') {
const cfg = Routes[name];
await guard(cfg);
const parsed = cfg.params ? cfg.params(params) : params;
navStore.stacks[domain].replace({ name, params: parsed, key: keyOf(name) });
renderHost(domain);
},
async pop(domain='global') {
const ok = navStore.stacks[domain].pop();
if (ok) renderHost(domain);
return ok;
},
async resetTo(name, params) {
const cfg = Routes[name]; await guard(cfg);
navStore.stacks.global.reset({ name, params, key: keyOf(name) });
renderHost('global');
},
async present(name, params) {
const cfg = Routes[name];
if (!cfg.modal) throw new Error('route is not modal');
await guard(cfg);
const parsed = cfg.params ? cfg.params(params) : params;
return new Promise<any>(async (resolve) => {
// 将 resolver 作为“结果回传器”放到顶层 params
navStore.stacks.modal.push({ name, params: { ...parsed, __resolver: resolve }, key: keyOf(name) });
renderHost('modal');
});
},
async handleDeepLink(url) {
// 解析:/detail?id=123 -> push('detail',{id:'123'})
const { name, params } = parseDeepLink(url);
return this.push(name, params, Routes[name].modal ? 'modal' : 'global');
},
current(domain='global') { return navStore.stacks[domain].top ?? null; }
};
async function guard(cfg: any) {
if (cfg?.canActivate && !(await cfg.canActivate())) {
// 失败时按策略跳登录
await Router.push('login');
throw new Error('guard blocked');
}
}
// 渲染主机(不同域各有宿主)
function renderHost(domain: NavDomain) {
// 在 ArkUI 中:驱动对应域的 NavHost 组件刷新(例如使用 @Observed / AppStorage)
globalThis.navHost?.notify(domain);
}
4. 参数传递与结果回传:比“全局变量”体面太多
4.1 进入页读取参数(类型安全)
// /features/detail/DetailPage.ets
@Component
export struct DetailPage {
private route = Router.current('global'); // 或注入
private id: string = this.route?.params?.id ?? '';
build() {
Column() {
Text(`Item #${this.id}`)
Button('Crop Photo in Modal')
.onClick(async () => {
const result = await Router.present('modal.confirm', { title: 'Use this photo?' });
if (result?.ok) { /* do save */ }
})
}
}
}
4.2 模态域内继续导航 & 结果回传
// /features/photo/ConfirmModal.ets
@Component
export struct ConfirmModal {
private route = Router.current('modal');
private resolver: (v:any)=>void = this.route?.params?.__resolver;
build() {
Column() {
Text('Confirm?')
Row() {
Button('Cancel').onClick(() => { this.resolver?.({ ok:false }); Router.pop('modal'); });
Button('Use').onClick(() => { this.resolver?.({ ok:true }); Router.pop('modal'); });
}
}.padding(16)
}
}
要点:模态域是独立栈;
present()返回一个 Promise,关闭时一次性回传,业务代码自然地await。
5. 深链(Deep Link):URL→路由→状态
5.1 解析器
// /router/deeplink.ts
import { RouteName } from './routes';
export function parseDeepLink(url: string): { name: RouteName; params: any } {
const u = new URL(url);
// 例:myapp://detail?id=123
if (u.pathname === '/detail') {
return { name: 'detail', params: { id: u.searchParams.get('id') } };
}
if (u.pathname === '/home') { return { name: 'home', params: {} }; }
// ……其余映射
return { name: 'splash', params: {} };
}
5.2 冷/暖启动打通
- 冷启动:Ability
onCreate→Router.handleDeepLink(intentUrl)→resetTo或push; - 暖启动:前台时收到深链 → 直接
push到目标域。
6. 返回行为一致性:策略 > 条件判断地狱
原则:单一出口处理返回键/手势 → 根据“最先可消费的域”依次尝试:模态域 → 局部容器(当前 Tab)→ 全局;若全栈仅剩根页,则执行最小化/退出策略。
// /router/BackPolicy.ts
import { Router } from './RouterImpl';
import { navStore } from './NavState';
export async function back(): Promise<void> {
// 1) 先关模态
if (navStore.stacks.modal.size > 0) {
await Router.pop('modal'); return;
}
// 2) 再看当前 Tab 的局部容器
const currentTab: 'home'|'explore'|'profile' = globalThis.tabStore.current();
if (navStore.stacks[currentTab].size > 1) {
await Router.pop(currentTab); return;
}
// 3) 最后全局
if (navStore.stacks.global.size > 1) {
await Router.pop('global'); return;
}
// 4) 根:最小化或询问退出
await minimizeOrExit();
}
async function minimizeOrExit() {
// 依据设计:提示一次 or 直接最小化
}
好处:无论入口路径多复杂,返回路径都可预测,测试只需覆盖策略分支而非散落在每个页面的 if/else。
7. 复杂场景下的路由解耦:Coordinator & Feature Router
当一个业务流程跨多个页面(甚至跨域)时,用**协调器(Coordinator)**收口:
// /flows/onboarding/OnboardingCoordinator.ts
export class OnboardingCoordinator {
async start() {
await Router.push('home'); // 1. 进入首页
const ok = await Router.present('modal.confirm', { title: 'Enable Sync?' }); // 2. 模态授权
if (ok) await Router.push('home.result', { q: 'synced' }, 'home'); // 3. 局部域跳转
// 4. 可能还有后续……
}
}
收益:把“流程路线图”和“UI 细节”分开;测试时直接调用
start()验证导航脚本是否符合预期。
8. 局部 Navigation(Tab/弹窗/侧栏)实战片段
8.1 Tabs 外壳 + 局部容器
// /shell/TabsShell.ets
@Entry
@Component
export struct TabsShell {
@State current: 'home'|'explore'|'profile' = 'home';
build() {
Column() {
// 内容区:根据 current 渲染对应 LocalNavHost
if (this.current === 'home') { LocalNavHost({ domain: 'home' }); }
if (this.current === 'explore'){ LocalNavHost({ domain: 'explore'}); }
if (this.current === 'profile'){ LocalNavHost({ domain: 'profile'}); }
// 底部 TabBar
Row() {
Button('Home').onClick(()=> this.current='home');
Button('Explore').onClick(()=> this.current='explore');
Button('Profile').onClick(()=> this.current='profile');
}.justifyContent(FlexAlign.SpaceAround)
}
}
}
// /router/LocalNavHost.ets
@Component
export struct LocalNavHost {
@Prop domain: NavDomain;
build() {
// 依据 navStore.stacks[this.domain].top 渲染对应页面
const top = Router.current(this.domain);
switch (top?.name) {
case 'home': HomePage(); break;
case 'home.filter': FilterPage(top.params); break;
case 'home.result': ResultPage(top.params); break;
// …… 其余 case
default: HomePage();
}
}
}
8.2 模态域宿主(带遮罩与动画)
// /router/ModalHost.ets
@Component
export struct ModalHost {
build() {
const top = Router.current('modal');
if (!top) return;
// 半透明遮罩 + 顶层页面
Stack() {
// dim
Rect().fillOpacity(0.4).width('100%').height('100%')
.onClick(()=> Router.pop('modal')); // 点击空白关闭(按需)
// modal body
Column().width('90%').height('auto').padding(16).borderRadius(12) {
switch (top.name) {
case 'modal.photo': PhotoPicker(); break;
case 'modal.crop': CropPage(); break;
case 'modal.confirm': ConfirmModal(); break;
}
}
}
}
}
9. 测试钩子与埋点:让导航“看得见”
9.1 导航脚本可重放(无 UI)
// /tests/navigation.spec.ts
import { Router } from '../router/RouterImpl';
import { navStore } from '../router/NavState';
test('deep link to detail pushes detail on global stack', async () => {
await Router.resetTo('tabs');
await Router.handleDeepLink('myapp://detail?id=42');
expect(navStore.stacks.global.snapshot().map(x=>x.name)).toEqual(['tabs', 'detail']);
});
9.2 埋点建议
nav_push:{ from, to, domain, params_size }nav_pop:{ domain, remain }deeplink_open:{ url, matched_route, cold/warm }modal_present/close:{ route, duration }- 异常:守卫阻断、参数校验失败。
10. 常见坑位 & 处方签
-
所有跳转都走全局栈 → Tab 内返回把用户扔回桌面。
- ✅ 每个 Tab 自带子栈,离开 Tab 不销毁栈。
-
模态里直接改全局路由 → 关闭模态后状态错乱。
- ✅ 模态域独立;若需跨域,先关闭模态再在回调里全局跳转。
-
参数随手丢对象 → 回放、埋点、深链都失真。
- ✅ 参数序列化可还原;定义
params()校验与精简。
- ✅ 参数序列化可还原;定义
-
返回逻辑分散 → 各写各的
if (canGoBack) ...。- ✅ 后退统一出口,策略层决定“谁先消费”。
-
深链只到页不还原状态 → 用户看见“空白详情”。
- ✅ 深链需构造必要上下文(例如先
tabs再detail)。
- ✅ 深链需构造必要上下文(例如先
11. 上线前 Checklist ✅
- 路由表声明完整(path/params/守卫/modal 标记)
- 多域多栈建立:
global / modal / tabX... -
present()结果回传链路覆盖测试 - 深链解析 + 冷/暖启动打通
- 统一返回策略 + 根场景最小化/退出策略
- 埋点:push/pop/deeplink/modal 全覆盖
- E2E 导航脚本(关键用户旅程可重放)
12. 收尾一句“人话”
导航的本质是叙事:用户从哪来、要去哪、为什么能回来。你把“地图(全局容器)”和“罗盘(局部容器)”都校准了,剩下就是让故事讲顺。到这一步,返回键不再是惊吓键,深链不再是赌博,模态不再是黑洞。
…
(未完待续)
更多推荐




所有评论(0)