我是兰瓶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 onCreateRouter.handleDeepLink(intentUrl)resetTopush
  • 暖启动:前台时收到深链 → 直接 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. 常见坑位 & 处方签

  1. 所有跳转都走全局栈 → Tab 内返回把用户扔回桌面。

    • 每个 Tab 自带子栈,离开 Tab 不销毁栈。
  2. 模态里直接改全局路由 → 关闭模态后状态错乱。

    • ✅ 模态域独立;若需跨域,先关闭模态再在回调里全局跳转。
  3. 参数随手丢对象 → 回放、埋点、深链都失真。

    • ✅ 参数序列化可还原;定义 params() 校验与精简。
  4. 返回逻辑分散 → 各写各的 if (canGoBack) ...

    • ✅ 后退统一出口,策略层决定“谁先消费”。
  5. 深链只到页不还原状态 → 用户看见“空白详情”。

    • ✅ 深链需构造必要上下文(例如先 tabsdetail)。

11. 上线前 Checklist ✅

  • 路由表声明完整(path/params/守卫/modal 标记)
  • 多域多栈建立:global / modal / tabX...
  • present() 结果回传链路覆盖测试
  • 深链解析 + 冷/暖启动打通
  • 统一返回策略 + 根场景最小化/退出策略
  • 埋点:push/pop/deeplink/modal 全覆盖
  • E2E 导航脚本(关键用户旅程可重放)

12. 收尾一句“人话”

导航的本质是叙事:用户从哪来、要去哪、为什么能回来。你把“地图(全局容器)”和“罗盘(局部容器)”都校准了,剩下就是让故事讲顺。到这一步,返回键不再是惊吓键,深链不再是赌博,模态不再是黑洞。

(未完待续)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐