HarmonyOS 6 Navigation 实战:NavPathStack 路由架构与 onShown 跨页状态同步方案

1、前言

🎉 新作已上架 ------ 「古诗学习宝 」鸿蒙原生应用已在华为应用市场上架,搜索「古诗学习宝」即可下载体验。零广告 / 零内购 / 277 首小学必背古诗全收录,烦请帮忙点个五星 🌟。

写鸿蒙应用最绕不开的两个问题:

  1. 多页面跳转怎么管才不乱? ------ 13 个详情页 + 4 种背诵模式 + 弹窗式 NavDestination,如果每个页面都用 router.pushUrl 散落各处,参数 Record<string,any> 满天飞,半年后回来改代码自己都看不懂。
  2. 详情页改了状态,怎么让首页知道? ------ 用户在 PoemDetailPage 点了收藏,pop 回首页后 PoemCard 的星标得变成实心,这个"跨页面状态同步"问题是 ArkUI V2 里最常见的坑,不少人栽在"V2 数组重赋值不触发刷新"上。

本文用「古诗学习宝」线上版本的真实做法,把这两个问题一次讲透:

  • 统一 Navigation 根 + PageMap 路由表:13 个页面只在一个文件注册,参数对象化强类型
  • NavPathStack.pushPathByName 替代 router.pushUrl:避免 deprecated 警告 + 类型安全
  • onShown + favVersion 版本号双保险 :从详情页返回首页时自动重算 @Computed,不丢任何状态变化
  • 真实审核反馈带来的 bug 修复 :V2 数组重赋值不触发刷新的解决方案------[...spread] 强引用 + 立即本地 filter

代码全部摘自上架版本,可直接复制到任何 HarmonyOS 6 项目。


2、整体架构

2.1 技术架构图

复制代码
┌──────────────────────────────────────────────────────────────────┐
│                      App 根入口 (Index.ets)                       │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │ @Entry @ComponentV2                                        │ │
│  │ struct Index {                                             │ │
│  │   @Local pathStack: NavPathStack = new NavPathStack();    │ │
│  │   @Local favVersion: number = 0;   // 跨页同步版本号       │ │
│  │   @Local avatarRef: string = '';                           │ │
│  │                                                            │ │
│  │   Navigation(this.pathStack) {                             │ │
│  │     MainTabsPage({                                         │ │
│  │       pathStack: this.pathStack,                           │ │
│  │       favVersion: this.favVersion,                         │ │
│  │       avatarRef: this.avatarRef,                           │ │
│  │       onAvatarChange: (ref) => { this.avatarRef = ref; }  │ │
│  │     })                                                     │ │
│  │   }                                                        │ │
│  │   .navDestination(this.PageMap)   // ★ 路由表             │ │
│  │ }                                                          │ │
│  └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────┬───────────────────────────────┘
                                   │ pushPathByName(name, params)
                                   ▼
┌──────────────────────────────────────────────────────────────────┐
│   PageMap @Builder (路由表) --- 13 个 NavDestination 子页面          │
│                                                                  │
│  if (name === RouteName.PoemDetail) {                            │
│    PoemDetailPage({                                              │
│      pathStack: this.pathStack,                                  │
│      onFavChange: () => { this.favVersion++; }   // ★ 关键回调  │
│    })                                                            │
│  } else if (name === RouteName.PoemList) {                       │
│    PoemListPage({ pathStack: this.pathStack })                   │
│  } else if (name === RouteName.ReciteFill) { ... }               │
│  // ...                                                          │
└──────────────────────────────────┬───────────────────────────────┘
                                   │ 用户点收藏
                                   ▼
┌──────────────────────────────────────────────────────────────────┐
│   PoemDetailPage 内部                                            │
│                                                                  │
│  toggleFavorite(id) → await FavoriteService.toggle(id)          │
│                    → this.onFavChange()  ← 通知 Index            │
│                                                                  │
│   Index.favVersion++                                             │
│       │                                                          │
│       ▼ @Param favVersion 透传                                   │
│   HomeView / ProfileView / PoemListPage 等所有页面同步重渲染     │
└──────────────────────────────────────────────────────────────────┘

2.2 项目目录

复制代码
entry/src/main/ets/
├── common/Constants.ets          # RouteName 路由名常量
├── pages/
│   ├── Index.ets                 # ★ 单一 Navigation 根 + PageMap
│   ├── MainTabsPage.ets          # 底部 5 Tab 容器
│   ├── CategoryPage.ets          # 分类
│   ├── PoemListPage.ets          # 诗词列表(按年级/主题/诗人/收藏)
│   ├── PoemDetailPage.ets        # 诗词详情(收藏入口)
│   ├── ReciteModePage.ets        # 背诵方式选择
│   ├── ReciteFillPage.ets        # 填空背诵
│   ├── ReciteChoicePage.ets      # 选择题背诵
│   ├── ReciteConnectPage.ets     # 连线题背诵
│   ├── ReciteListenPage.ets      # 听音造句背诵
│   ├── CharStudyPage.ets         # 生字学习
│   ├── AchievementsPage.ets      # 成就奖章
│   └── FeedbackPage.ets          # 意见反馈
└── service/FavoriteService.ets   # 收藏单例服务

2.3 架构原则

原则 说明
单一 Navigation 根 整个 App 只有 1 个 Navigation 组件实例,所有页面都是 NavDestination
路由参数对象化 interface XxxRouteParam 强类型替代 Record<string,any>
页面持有 pathStack 引用 通过 @Param 从父级透传,子页面用它做 push/pop
跨页同步走版本号 收藏/学习记录等"会被多页面共享"的状态用 @Param favVersion: number 触发重算
onShown 回调兜底 从子页面返回时,列表页 onShown 重新读 Service,处理边角场景

3、效果展示

3.1 首页 - 多入口跳详情

首页有至少 5 种跳转入口 全部通向 PoemDetailPage:① 今日推荐卡片 ② 8 快捷入口里的"今日一首"③"随机一首"④ 猜你喜欢列表 ⑤ 收藏列表(点星标进详情)。5 种入口共享同一个 pushPathByName(RouteName.PoemDetail, id) 调用,参数统一是诗词 ID。

3.2 诗词详情页 - 收藏交互

底部 3 按钮:收藏 (次按钮)/ 开始背诵(主按钮)/ 生字。点收藏后 FavoriteService.toggle(id) 写入 Preferences,同时通过 onFavChange 回调通知到 Index 根组件,favVersion++ 触发所有相关页面重算

3.3 返回首页 - 收藏数同步

返回到「我的」Tab,4 张统计卡里"收藏 1"自动同步 (不需要刷新)。这就是 favVersion 版本号驱动的 @Computed 重算效果。

3.4 学习记录页

记录 Tab 下三个子 tab:学习记录 / 背诵记录 / 错题本,每次进入页面 onShown 钩子重新从 RecordService 读取,保证从背诵页返回后数据立刻同步

3.5 分类页 - 子路由

点任意年级卡片 → pushPathByName(RouteName.PoemList, CategoryRouteParam) 进入诗词列表页,路由参数对象类型严格:

typescript 复制代码
const param: CategoryRouteParam = {
  type: 'grade',           // 'grade' | 'theme' | 'poet' | 'favorite'
  key: 'g1',
  label: '一年级',
  themes: [],              // 可选
};

4、核心功能详解

整个 App 入口 Index.ets

typescript 复制代码
import { RouteName, StorageKey } from '../common/Constants';
import { FavoriteService } from '../service/FavoriteService';
import { RecordService } from '../service/RecordService';
import { PlanService } from '../service/PlanService';
import { WrongQuestionService } from '../service/WrongQuestionService';
import { Pref } from '../service/PreferencesUtil';
import { MainTabsPage } from './MainTabsPage';
// 13 个 NavDestination 子页面
import { CategoryPage } from './CategoryPage';
import { PoemListPage } from './PoemListPage';
import { PoemDetailPage } from './PoemDetailPage';
import { ReciteModePage } from './ReciteModePage';
import { ReciteFillPage } from './ReciteFillPage';
import { ReciteChoicePage } from './ReciteChoicePage';
import { ReciteConnectPage } from './ReciteConnectPage';
import { ReciteListenPage } from './ReciteListenPage';
import { CharStudyPage } from './CharStudyPage';
import { AchievementsPage } from './AchievementsPage';
import { FeedbackPage } from './FeedbackPage';

@Entry
@ComponentV2
struct Index {
  /** ⭐ 核心:单一 NavPathStack 实例,整个 App 共享 */
  @Local pathStack: NavPathStack = new NavPathStack();
  /** ⭐ 跨页状态同步版本号(收藏/记录任意页面变化都 ++) */
  @Local favVersion: number = 0;
  /** 全局用户头像(透传到所有需要显示头像的页面) */
  @Local avatarRef: string = '';
  /** 闪屏控制 */
  @Local showSplash: boolean = true;

  async aboutToAppear(): Promise<void> {
    // 冷启并发加载 4 个 Service(覆盖所有持久化数据)
    await Promise.all([
      FavoriteService.load(),
      RecordService.load(),
      PlanService.load(),
      WrongQuestionService.load(),
    ]);
    this.avatarRef = await Pref.getString(StorageKey.UserAvatar, '');
    // 1 秒后关闭闪屏
    setTimeout(() => { this.showSplash = false; }, 1000);
  }

  build() {
    Stack() {
      Navigation(this.pathStack) {
        MainTabsPage({
          pathStack: this.pathStack,
          favVersion: this.favVersion,         // 透传版本号
          avatarRef: this.avatarRef,
          onAvatarChange: (ref: string) => this.onAvatarChange(ref),
        })
      }
      .navDestination(this.PageMap)            // ★ 注册路由表
      .hideTitleBar(true)
      .mode(NavigationMode.Stack)

      // 闪屏覆盖
      if (this.showSplash) {
        SplashPage()
      }
    }
    .width('100%').height('100%')
  }

  /** ★ 路由表:13 个 NavDestination 全部在这里注册 */
  @Builder
  PageMap(name: string) {
    if (name === RouteName.Category) {
      CategoryPage({ pathStack: this.pathStack })
    } else if (name === RouteName.PoemList) {
      PoemListPage({ pathStack: this.pathStack })
    } else if (name === RouteName.PoemDetail) {
      PoemDetailPage({
        pathStack: this.pathStack,
        onFavChange: () => { this.favVersion++; },   // ⭐ 关键回调
      })
    } else if (name === RouteName.ReciteMode) {
      ReciteModePage({ pathStack: this.pathStack })
    } else if (name === RouteName.ReciteFill) {
      ReciteFillPage({ pathStack: this.pathStack })
    } else if (name === RouteName.ReciteChoice) {
      ReciteChoicePage({ pathStack: this.pathStack })
    } else if (name === RouteName.ReciteConnect) {
      ReciteConnectPage({ pathStack: this.pathStack })
    } else if (name === RouteName.ReciteListen) {
      ReciteListenPage({ pathStack: this.pathStack })
    } else if (name === RouteName.CharStudy) {
      CharStudyPage({ pathStack: this.pathStack })
    } else if (name === RouteName.Achievements) {
      AchievementsPage({ pathStack: this.pathStack })
    } else if (name === RouteName.Feedback) {
      FeedbackPage({ pathStack: this.pathStack })
    }
  }

  async onAvatarChange(ref: string): Promise<void> {
    this.avatarRef = ref;
    await Pref.setString(StorageKey.UserAvatar, ref);
  }
}

坑点 1Navigation(this.pathStack) 必须传一个 @Local NavPathStack 实例,不能在组件外定义 。每次构造的 NavPathStack 都是新的,组件内必须用 @Local 持有它。
坑点 2navDestination(this.PageMap) 必须传 @Builder 函数引用,不能传字符串路由表(这不是 React Router)。这意味着所有 NavDestination 子组件的 import 都在 Index.ets 里,编译期就能发现路由问题

4.2 第二步:RouteName 集中管理路由名

typescript 复制代码
// common/Constants.ets
export class RouteName {
  static readonly Splash        = 'SplashPage';
  static readonly MainTabs      = 'MainTabsPage';
  static readonly Category      = 'CategoryPage';
  static readonly PoemList      = 'PoemListPage';
  static readonly PoemDetail    = 'PoemDetailPage';
  static readonly ReciteMode    = 'ReciteModePage';
  static readonly ReciteFill    = 'ReciteFillPage';
  static readonly ReciteChoice  = 'ReciteChoicePage';
  static readonly ReciteConnect = 'ReciteConnectPage';
  static readonly ReciteListen  = 'ReciteListenPage';
  static readonly CharStudy     = 'CharStudyPage';
  static readonly StudyPlan     = 'StudyPlanPage';
  static readonly StudyRecord   = 'StudyRecordPage';
  static readonly Achievements  = 'AchievementsPage';
  static readonly Feedback      = 'FeedbackPage';
}

集中常量的好处:

  • 全局重命名 AchievementsAwards 改一处即可
  • IDE 自动补全防止打字错误('AchivementsPage' 这种)
  • 编译期发现"路由名拼错"问题

4.3 第三步:路由参数对象化强类型

跨页面传参不要用 Record<string, any>,而是用 interface:

typescript 复制代码
// pages/CategoryPage.ets
export interface CategoryRouteParam {
  type: 'grade' | 'theme' | 'poet' | 'favorite';
  key: string;
  label: string;
  themes?: string[];
}

// 触发跳转
this.pathStack.pushPathByName(
  RouteName.PoemList,
  { type: 'grade', key: 'g1', label: '一年级' } as CategoryRouteParam,
);

// 在 PoemListPage 接收
aboutToAppear(): void {
  const params = this.pathStack.getParamByName(RouteName.PoemList) as object[];
  const p = (params && params.length > 0
              ? params[params.length - 1]
              : null) as CategoryRouteParam | null;
  if (!p) return;
  // 之后访问 p.type / p.key / p.label 都有 TypeScript 类型保护
}

坑点 3getParamByName 返回的是 object[](栈中所有同名路由的参数),不是单个对象。记得取最后一个 params[params.length - 1]

4.4 第四步:跨页状态同步的 favVersion 版本号

V2 模式下,"详情页改了状态、列表页要刷新"的标准做法不是用 EventBus,而是用一个简单的 number 版本号

typescript 复制代码
// Index.ets(根组件)
@Local favVersion: number = 0;

// PageMap 注册 PoemDetail 时
PoemDetailPage({
  pathStack: this.pathStack,
  onFavChange: () => { this.favVersion++; },   // 每次收藏切换 +1
})

// MainTabsPage 透传
MainTabsPage({
  pathStack: this.pathStack,
  favVersion: this.favVersion,   // ← 透传
  avatarRef: this.avatarRef,
  onAvatarChange: ...
})

子组件用 @Param 接收 + @Computed 派生:

typescript 复制代码
// pages/views/ProfileView.ets
@ComponentV2
export struct ProfileView {
  @Param favVersion: number = 0;     // ← 接收

  @Computed
  get stat(): StudyStat {
    const _v = this.favVersion;       // ⭐ 显式依赖 favVersion,触发重算
    return RecordService.stat();
  }

  build() {
    Row({ space: 8 }) {
      StatCard({ label: '连续天数', value: `${this.stat.streak}` })
      StatCard({ label: '已学诗词', value: `${this.stat.totalPoems}` })
      StatCard({ label: '收藏',     value: `${this.stat.favoriteCount}` })
      StatCard({ label: '总天数',   value: `${this.stat.totalDays}` })
    }
  }
}

坑点 4@Computed 必须显式读取 依赖的 @Param(即使你不真的用它),否则不会重新计算。这里 const _v = this.favVersion 看起来无用,实际是让 V2 编译器知道这个 Computed 依赖 favVersion------少了这行,favVersion 变化时不会重算 stat。

4.5 第五步:onShown 回调兜底列表刷新

不是所有数据都适合用 @Computed 派生,比如:

  • 收藏列表 PoemListPage 的 list: PoemBrief[]
  • 错题本 RecordView 的 wrongList: WrongQuestion[]

这些是"具体数据数组",不是"派生统计",需要在 onShown 重新读:

typescript 复制代码
// pages/PoemListPage.ets
@ComponentV2
export struct PoemListPage {
  @Param pathStack: NavPathStack = new NavPathStack();
  @Local title: string = '';
  @Local list: PoemBrief[] = [];
  @Local pageType: string = '';

  aboutToAppear(): void {
    const params = this.pathStack.getParamByName(RouteName.PoemList) as object[];
    const p = (params && params.length > 0
                ? params[params.length - 1]
                : null) as CategoryRouteParam | null;
    if (!p) return;
    this.title = p.label;
    this.pageType = p.type;
    if (p.type === 'favorite') {
      this.list = PoemService.briefByIds(FavoriteService.list());
    } else if (p.type === 'grade') {
      this.list = PoemService.byGrade(parseInt(p.key.replace('g', '')));
    }
    // ...
  }

  /** ⭐ 收藏列表专用刷新:从详情页 pop 回来时重新读 Service */
  refreshIfFavorite(): void {
    if (this.pageType === 'favorite') {
      // 用 spread 强制新数组引用,确保 V2 reactive 触发 ForEach diff
      this.list = [...PoemService.briefByIds(FavoriteService.list())];
    }
  }

  build() {
    NavDestination() {
      // ...
      List() {
        ForEach(this.list, (p: PoemBrief) => {
          ListItem() {
            PoemCard({
              poem: p,
              favorite: FavoriteService.isFav(p.id),
              onTap: (id: string) => this.pathStack.pushPathByName(RouteName.PoemDetail, id),
              onFav: (id: string) => this.toggleFav(id),
            })
          }
        }, (p: PoemBrief) => p.id)
      }
    }
    .hideTitleBar(true)
    .onShown(() => this.refreshIfFavorite())   // ⭐ 每次显示触发刷新
  }

  async toggleFav(id: string): Promise<void> {
    await FavoriteService.toggle(id);
    // ⭐ 立即从本地 list 移除被取消的项,保证 V2 立即重渲染
    if (this.pageType === 'favorite' && !FavoriteService.isFav(id)) {
      this.list = this.list.filter((p: PoemBrief) => p.id !== id);
    }
    this.refreshIfFavorite();
  }
}

坑点 5 :V2 @Local list: T[] 数组重赋值在某些边角场景不会触发 ForEach diff 。审核员就遇到了"取消收藏后列表没更新"的 bug。解决方案双保险:

立即本地 filter 移除 (保证视觉上立即响应)

[...spread] 强制新数组引用(保证 service 端最终一致)

「古诗学习宝」有 5 个底部 Tab 和 13 个 NavDestination 子页面。两者职责严格区分:

  • 底部 Tab 切换 :通过 MainTabsPage.@Local selectedKey 直接切换显示哪个 View,不入 NavPathStack
  • 子页面跳转 :通过 pathStack.pushPathByName 入栈,返回手势走系统左滑或 pop
typescript 复制代码
// pages/MainTabsPage.ets
@Local selectedKey: string = TabKey.Home;

build() {
  Column() {
    Stack() {
      if (this.selectedKey === TabKey.Home) HomeView({ ... })
      else if (this.selectedKey === TabKey.Category) CategoryPage({ ..., embedded: true })
      // ...
    }
    .layoutWeight(1)

    BottomTabBar({
      selectedKey: this.selectedKey,
      onChange: (k: string) => { this.selectedKey = k; },
    })
  }
}

CategoryPage 在 Tab 内嵌时 embedded: true,在 NavPathStack 子页面时 embedded: false------同一个组件根据 embedded 参数决定是否显示 NavBar,复用代码。

4.7 第七步:从子页面跳转到 Tab 的特殊情况

「我的」页面点「我的收藏」要跳到 PoemList 的 favorite 模式。这是从 Tab 内的 View 跳出去到 NavDestination:

typescript 复制代码
// pages/views/ProfileView.ets
openFavorites(): void {
  const param: CategoryRouteParam = {
    type: 'favorite',
    key: 'fav',
    label: '我的收藏',
  };
  this.pathStack.pushPathByName(RouteName.PoemList, param);
}

// 而点"成就奖章"则不需要参数
openAchievements(): void {
  this.pathStack.pushPathByName(RouteName.Achievements, null);
}

坑点 6pushPathByName 第二个参数即使不需要也必须传 null 或 undefined,不能省略。否则 TypeScript 编译报错。

4.8 第八步:从子页面切回 Tab

PoemDetail 里可能有「返回首页」入口,这时要:① pop 到根 ② 把 Tab 切到 Home。

typescript 复制代码
// PoemDetailPage 内部
backToHome(): void {
  // ① 清空 NavPathStack
  this.pathStack.clear();
  // ② 通过 AppStorage 传递 "请切到 Home Tab"
  AppStorage.setOrCreate('pending_tab_switch', TabKey.Home);
}

// MainTabsPage.aboutToAppear 或 onShown 接收
async onShown(): Promise<void> {
  const pending = AppStorage.get<string>('pending_tab_switch');
  if (pending) {
    this.selectedKey = pending;
    AppStorage.setOrCreate('pending_tab_switch', '');
  }
}

AppStorage 是 ArkUI 内置的全局 KV,作为"Navigation 栈与底部 Tab 之间通信的桥梁"最合适。


5、完整数据流分析

以「首页点收藏 → 详情页点收藏 → pop 回首页 → 我的页统计同步」为例:

复制代码
冷启
    │
    ▼
Index.aboutToAppear
    ├─ Promise.all([FavoriteService.load(), ...]) 4 个 Service 并发
    └─ pathStack = new NavPathStack()
            │
            ▼
    Navigation(pathStack) 渲染 MainTabsPage
            │
            ▼
    MainTabsPage.@Local selectedKey = TabKey.Home
            │
            ▼
    HomeView 显示,@Param favVersion=0 透传
─────────────────────────────────────────────────────────────────
用户点首页"今日一首"
    │
    ▼
HomeView.openTodayPoem()
    └─ this.pathStack.pushPathByName(RouteName.PoemDetail, '夏日绝句')
            │
            ▼
Index.PageMap('PoemDetailPage')
    └─ PoemDetailPage({
         pathStack,
         onFavChange: () => { this.favVersion++; }  // 闭包持有 Index
       })
─────────────────────────────────────────────────────────────────
PoemDetailPage 渲染 → 用户点"收藏"按钮
    │
    ▼
toggleFavorite()
    ├─ await FavoriteService.toggle(id)
    │      └─ cache.add(id) + Preferences.flush()
    └─ this.onFavChange()   // 通知 Index
            │
            ▼
Index.@Local favVersion: 0 → 1
            │
            ▼  V2 触发所有 @Param favVersion 的组件重渲染
    HomeView.favVersion: 0 → 1
        └─ @Computed stat 重算
                ↓ stat.favoriteCount: 0 → 1
                ↓ UI 自动刷新(但首页此时不可见)
    ProfileView.favVersion: 0 → 1
        └─ 同样重算
─────────────────────────────────────────────────────────────────
用户按系统左滑返回手势
    │
    ▼
NavPathStack 自动 pop PoemDetailPage
    │
    ▼
HomeView.onShown 触发(页面重新可见)
    └─ 此时 favVersion 已经是 1,@Computed stat 早就重算完
            │
            ▼  PoemCard 通过 FavoriteService.isFav(p.id) 同步显示星标
─────────────────────────────────────────────────────────────────
用户点底部"我的" Tab
    │
    ▼
MainTabsPage.selectedKey = 'profile'
    │
    ▼
ProfileView 显示
    └─ @Computed stat = { favoriteCount: 1, ... }(无需任何刷新)
            ↓
    4 张统计卡:连续天数 1 / 已学诗词 1 / 收藏 1 / 总天数 1
                                          ↑ 已同步

观察点:

  1. 冷启 4 Service 并发 load :Promise.all 让 4 个持久化 Service 同时加载,比串行快 4 倍
  2. favVersion ++ 是状态同步唯一驱动力 :所有"会被多页面共享的派生数据"全部依赖它,一处变化、全局同步
  3. @Computed 重算可在不可见页面发生 :HomeView 此时被 PoemDetail 遮挡,但 @Computed 仍然算了一次------等返回时直接展示,零延迟。
  4. onShown 用作 ForEach 数组重读兜底 :派生统计走 @Computed,具体列表走 onShown,两条数据流互补

6、代码分析与优化建议

6.1 现有实现的亮点

  • 单一 Navigation 根:13 个页面在一处注册,路由问题集中可控
  • 路由参数对象化:interface CategoryRouteParam 让参数有类型保护
  • favVersion 版本号驱动 @Computed:派生统计自动同步,无需 EventBus
  • onShown + filter 双保险:解决 V2 数组重赋值边角问题
  • AppStorage 桥接 Tab + Nav 栈:处理"返回首页切 Tab"特殊场景

6.2 可优化点

优化 1:路由表 Map<RouteName, Builder> 替代 if-else

问题:13 个 if-else 分支冗长,新增页面要改 Index.ets。

改进:把路由表抽到独立的 routes.ets:

typescript 复制代码
// pages/routes.ets
export type PageBuilder = (name: string, stack: NavPathStack) => void;

export const ROUTE_TABLE: Record<string, PageBuilder> = {
  [RouteName.Category]:    (name, stack) => CategoryPage({ pathStack: stack }),
  [RouteName.PoemList]:    (name, stack) => PoemListPage({ pathStack: stack }),
  // ...
};

// Index.ets
@Builder
PageMap(name: string) {
  const builder = ROUTE_TABLE[name];
  if (builder) builder(name, this.pathStack);
}

取舍 :@Builder 内部不支持 Map 查找直接调用 Builder,所以这个优化在 V2 上目前不可行------仍然得用 if-else。等 ArkUI 升级。

优化 2:把 favVersion 改成 StateStore 全局态

问题:favVersion 透传链路长:Index → MainTabs → ProfileView,新增页面要加 @Param。

改进 :用 HarmonyOS 6 的 StateStore 做全局态:

typescript 复制代码
import { StateStore } from '@kit.ArkUI';

class AppState {
  favVersion: number = 0;
}
export const appState = StateStore.create(new AppState());

// 任意组件
@Computed
get stat(): StudyStat {
  const _v = appState.read('favVersion');
  return RecordService.stat();
}

// 修改
appState.write('favVersion', appState.read('favVersion') + 1);
优化 3:用 Bundle Manager 做路由按需加载

问题:13 个页面 import 全在 Index.ets 顶部,冷启加载所有 .ets 文件。

改进 :用 import() 动态导入大页面,减少首屏体积:

typescript 复制代码
@Builder
PageMap(name: string) {
  if (name === RouteName.ReciteFill) {
    LazyLoadPage({ name: 'ReciteFillPage', stack: this.pathStack })
  }
}
优化 4:路由埋点统一处理

问题:每个页面 push 时手动加埋点,容易漏。

改进:包装 pathStack 为 InstrumentedNavPathStack:

typescript 复制代码
class InstrumentedNavPathStack extends NavPathStack {
  pushPathByName(name: string, params?: object): void {
    hilog.info(0xC0DE, 'Route', 'push: %{public}s', name);
    // 上报埋点
    Analytics.track('page_view', { name, params });
    super.pushPathByName(name, params);
  }
}
优化 5:@Computed 依赖 @Param 必须显式读取

问题@Computed get x() { return service.calc(); } 没有读 favVersion,永远不会重算

改进:标准模板:

typescript 复制代码
@Computed
get stat(): StudyStat {
  const _depV = this.favVersion;   // 显式声明依赖(即使不用)
  return RecordService.stat();
}

或者用更优雅的写法 - 把 favVersion 作为参数传给 service:

typescript 复制代码
@Computed
get stat(): StudyStat {
  return RecordService.statAtVersion(this.favVersion);
}

// RecordService
statAtVersion(_version: number): StudyStat {
  return this.stat();   // version 参数仅用于触发 @Computed 重算
}

6.3 生产环境 Checklist

检查项 说明
整个 App 只有 1 个 Navigation 根 避免嵌套 Navigation 导致返回手势冲突
pathStack: NavPathStack@Local 持有 不能在组件外部定义
所有路由名走 RouteName.XXX 常量 防止字符串拼错
路由参数用 interface 强类型 Record<string,any> 容易隐藏 bug
getParamByName 返回 object[] 取最后一个 不是单个对象
跨页同步用 @Param favVersion 而非 EventBus V2 reactive 自动重算
@Computed 必须显式读取依赖的 @Param 否则不重算
列表数组用 onShown + [...spread] 兜底 避免 V2 数组重赋值不刷新边角问题
取消项立即 filter 移除 视觉上立即响应
Tab 切换用 @Local selectedKey,不入 NavStack 底部 Tab 切换不应有返回栈
子页面跳 Tab 用 AppStorage 桥接 NavStack 与 Tab 切换的解耦方案

7、关键 API 速查

API 作用
Navigation(pathStack) 路由容器
NavPathStack 路由栈实例
pathStack.pushPathByName(name, params?) 入栈跳转
pathStack.pop() / pathStack.clear() 出栈 / 清空
pathStack.getParamByName(name) 获取参数数组(取最后一个)
.navDestination(builder) 注册路由表(@Builder)
NavDestination() 子页面容器
.hideTitleBar(true) 隐藏默认 NavBar
.mode(NavigationMode.Stack) 栈模式(默认)
.onShown(() => ...) 页面显示回调
@Entry @ComponentV2 V2 入口组件
@Local, @Param, @Event, @Computed V2 状态装饰器
AppStorage.setOrCreate(key, value) 全局 KV 存储
AppStorage.get<T>(key) 全局 KV 读取

8、总结

本文用「古诗学习宝」线上版本的真实做法,把鸿蒙 6 应用最容易踩坑的两个问题------多页面路由管理 + 跨页状态同步------一次讲透:

  1. 单一 Navigation 根 + PageMap 路由表 :13 个 NavDestination 在一个 @Builder 函数里集中注册,所有路由问题编译期可见

  2. 路由参数强类型对象化 :用 interface CategoryRouteParam 替代 Record<string,any>,路由参数从"运行时崩溃"提前到"编译时报错"。

  3. favVersion 版本号驱动 @Computed :跨页同步不用 EventBus、不用 Provide/Consume,一个简单的 number 让 V2 reactive 自动重算------这是 HarmonyOS 6 V2 模式最干净的跨页同步方案。

  4. @Computed 必须显式读取依赖const _v = this.favVersion 看起来没用,实际是给 V2 编译器的依赖声明------少了这行,favVersion 变化时不会重算。

  5. onShown + [...spread] 双保险 :派生统计走 @Computed,具体列表数组用 onShown 重新读 Service + spread 强引用,规避 V2 数组重赋值不触发刷新的边角问题(这是真实审核反馈的 bug 修复经验)。

  6. 6 个真坑写进 Checklist:单一 Navigation / RouteName 常量 / interface 路由参数 / @Computed 显式依赖 / [...spread] / Tab 切换不入栈------任何一个踩错都会让用户体验断裂。

「古诗学习宝」上架版本的 13 个页面就是用这套方案串起来的,性能稳、bug 少、维护轻松。把这套「单一 Navigation + 路由参数对象化 + favVersion 版本号 + onShown 兜底」的范式吃透,任何 HarmonyOS 6 中等规模应用的路由架构都能稳如老狗。


🎁 下载体验

**「古诗学习宝」**已上架华为应用市场,搜索 古诗学习宝 即可下载。本文的 Navigation 路由架构 + 跨页同步方案在 App 13 页路由间真实可见:进收藏 → pop 回首页统计自动同步、做完背诵 → 错题本自动出现、改个头像 → 全局头像位置一起更新。

📚 文中代码全部摘自上架版本,可直接复制。

👋 欢迎在评论区聊聊你的鸿蒙路由架构方案,互相学习。

相关推荐
qcx231 小时前
【AI Agent实战】多 Agent 编排架构:五层模型与 RL 优化
网络·人工智能·ai·架构·prompt·agent
fengxin_rou1 小时前
Feed 三级缓存架构详解:分层设计、缓存一致性与高性能实战
spring·缓存·架构
code_pgf1 小时前
模态预融合(Modality-Pre-Fusion)在 sVLM 中的具体应用、优势及主要区别
人工智能·架构
GIOTTO情1 小时前
Infoseek字节探索传播溯源技术,解析危机公关舆情拓扑管控方案
架构
号码认证服务1 小时前
企业固话号码认证能覆盖哪些手机品牌?支持华为、小米、OPPO、vivo等机型
服务器·网络·经验分享·python·华为·智能手机·云计算
云水一下1 小时前
下一代防火墙(NGFW)完全解析:从入门到华为eNSP模拟器实战
网络·华为·下一代防火墙
我是小邵1 小时前
从 Supabase 迁移到 AWS 的云架构演进实践
架构·云计算·aws
KKei16382 小时前
Flutter for OpenHarmony 编程技能树APP技术文章
flutter·华为·harmonyos
想你依然心痛2 小时前
HarmonyOS 6(API 23)实战:基于Face AR呼吸监测与Body AR姿态引导的“静界空间“——PC端沉浸式冥想疗愈系统
华为·ar·harmonyos·悬浮导航·沉浸光感