鸿蒙原生应用实战(五):路由导航与工程优化 — 从开发到上线的完整流程

鸿蒙原生应用实战(五):路由导航与工程优化 --- 从开发到上线的完整流程

一、前言

经过前四篇的开发,我们的游戏收藏夹 App 已经拥有 5 个页面、1800+ 行 ArkTS 代码。本篇将从架构高度重新审视整个项目,涵盖:

  • 路由系统深度解析与导航架构设计
  • module.json5 配置详解
  • 构建配置与性能优化
  • 单元测试与 UI 测试实践
  • ArkTS 严格模式最佳实践
  • DevEco Studio 调试技巧

二、路由系统深度解析

2.1 路由架构概览

复制代码
                   Index (首页)
                 /     |     \
                ▼     ▼      ▼
         GameListPage  WishPage  StatsPage
                |
                ▼
          GameDetailPage

整个 App 的路由关系是星型拓扑:首页作为 Hub,可以跳转到任意子页面,详情页作为最深层级页面。

2.2 路由配置

main_pages.json 中注册所有页面路由:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/GameListPage",
    "pages/GameDetailPage",
    "pages/WishPage",
    "pages/StatsPage"
  ]
}

module.json5 中引用:

json 复制代码
{
  "module": {
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "launchType": "standard"
      }
    ]
  }
}

2.3 路由 API 全面解析

2.3.1 路由导入
typescript 复制代码
// API 23 下的正确导入方式
import router from '@ohos.router';

注意 :API 23 版本下,router@ohos.router 导入,而不是 @kit.AbilityKit。这个版本 @kit.AbilityKit 不导出 router API,如果用错会导致编译错误。

2.3.2 页面跳转
typescript 复制代码
// 不带参数的跳转
router.pushUrl({ url: 'pages/GameListPage' });

// 带参数的跳转
router.pushUrl({
  url: 'pages/GameDetailPage',
  params: { gameId: 1 }
});
2.3.3 接收参数
typescript 复制代码
// 参数接收的标准写法
const params = router.getParams() as Record<string, Object>;
if (params && params['filter'] !== undefined) {
  this.filter = params['filter'] as string;
}
2.3.4 返回上一页
typescript 复制代码
router.back();

2.4 项目中的路由调用汇总

源页面 目标页面 携带参数 触发方式
Index GameListPage { filter: string } 快速筛选标签点击
Index GameListPage 底部导航"游戏库"
Index WishPage 愿望单统计卡片 / 底部导航
Index StatsPage 底部导航"统计"
GameListPage GameDetailPage { gameId: number } 游戏卡片点击
WishPage GameDetailPage { gameId: number } 愿望单卡片点击
任意详情页 --- --- 返回按钮 → router.back()

2.5 导航架构设计模式

2.5.1 显式导航 vs 隐式导航

鸿蒙的 router.pushUrl 属于显式导航,直接指定目标页面 URL 和参数。优点是:

  • 类型安全(编译时校验页面路径)
  • 参数明确(通过 params 对象传递)
  • 调用链路清晰
2.5.2 导航栈管理

router.pushUrl 默认使用标准模式,每次跳转都入栈:

复制代码
初始:   [Index]
跳转:   [Index, GameListPage]
再跳转: [Index, GameListPage, GameDetailPage]
返回:   [Index, GameListPage]

这种栈式管理保证了:

  • router.back() 总能正确返回上一页
  • 支持系统返回按键
  • 避免页面层级过深(最多 3 层)

三、module.json5 深度配置

3.1 完整配置解读

json5 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",                     // module 类型:entry/feature/har
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",       // 主 Ability 入口
    "deviceTypes": ["phone"],
    "deliveryWithInstall": true,          // 随安装包交付
    "installationFree": false,           // 是否免安装
    "pages": "$profile:main_pages",      // 引用页面路由配置
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,                // 允许外部启动
        "skills": [                      // 隐式意图过滤
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      }
    ]
  }
}

3.2 关键字段解析

字段 作用
type entry 应用主入口模块
mainElement EntryAbility 指定入口 Ability
deliveryWithInstall true 随安装包一体交付
deviceTypes "phone" 仅支持手机
skills home intent 让 App 出现在桌面

3.3 $ 资源引用

鸿蒙使用 $ 前缀引用资源文件:

复制代码
$string:module_desc       → string.json 中的 module_desc
$media:layered_image      → media 目录下的图片资源
$color:start_window_background  → color.json 中的色值
$profile:main_pages       → profile 目录下的 main_pages.json

资源文件目录结构:

复制代码
resources/
├── base/
│   ├── element/
│   │   ├── string.json     // 字符串
│   │   ├── color.json      // 颜色
│   │   └── float.json      // 字号/尺寸
│   ├── media/              // 图片资源
│   └── profile/            // 配置文件
└── dark/
    └── element/
        └── color.json      // 暗色模式颜色覆盖

四、构建配置优化

4.1 项目级 build-profile.json5

json5 复制代码
{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "targetSdkVersion": "6.1.1(24)",
        "compatibleSdkVersion": "6.1.0(23)",
        "runtimeOS": "HarmonyOS",
        "buildOption": {
          "strictMode": {
            "caseSensitiveCheck": true,       // 大小写敏感检查
            "useNormalizedOHMUrl": true       // 使用规范 OHM 包 URL
          }
        }
      }
    ],
    "buildModeSet": [
      { "name": "debug" },
      { "name": "release" }
    ]
  }
}

strictMode 详解

caseSensitiveCheck: true --- 对 HarmonyOS 文件系统区分大小写的设备(如某些模拟器),确保 import 路径大小写一致。如果导入 from './pages/Index' 但实际文件是 index.ets,会报错。

4.2 模块级 build-profile.json5

json5 复制代码
{
  "apiType": "stageMode",
  "buildOption": {
    "resOptions": {
      "copyCodeResource": { "enable": false }
    }
  },
  "buildOptionSet": [
    {
      "name": "release",
      "arkOptions": {
        "obfuscation": {                 // 代码混淆
          "ruleOptions": {
            "enable": false,
            "files": ["./obfuscation-rules.txt"]
          }
        }
      }
    }
  ],
  "targets": [
    { "name": "default" },
    { "name": "ohosTest" }
  ]
}

4.3 构建命令

bash 复制代码
hvigorw --mode module \
  -p module=entry@default \
  -p product=default \
  -p requiredDeviceType=phone \
  assembleHap \
  --analyze=normal \
  --parallel \
  --incremental \
  --daemon

参数含义

参数 说明
--mode module 模块级构建
-p module=entry@default 构建 entry 模块的 default 目标
assembleHap 生成 HAP 安装包
--parallel 启用并行构建
--incremental 增量编译(仅编译变更文件)
--daemon 保持守护进程,加速后续构建

五、测试实践

5.1 测试目录结构

复制代码
entry/src/
├── main/                    # 源代码
└── test/                    # 本地单元测试
    ├── List.test.ets
    └── LocalUnit.test.ets

entry/src/ohosTest/          # 设备/UI 测试
└── ets/
    └── test/
        ├── Ability.test.ets
        └── List.test.ets

5.2 本地单元测试

typescript 复制代码
// LocalUnit.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { UIAbility } from '@kit.AbilityKit';

describe('GameDataTest', () => {
  it('test_filter_status', 0, () => {
    // 测试筛选逻辑的正确性
    const games = [
      { id: 1, title: 'Game A', status: '通关' },
      { id: 2, title: 'Game B', status: '在玩' }
    ];
    const filtered = games.filter(g => g.status === '通关');
    expect(filtered.length).assertEqual(1);
    expect(filtered[0].title).assertEqual('Game A');
  });

  it('test_calc_stats', 0, () => {
    // 测试统计数据计算
    const games = [
      { id: 1, hours: 100, status: '通关' },
      { id: 2, hours: 50, status: '在玩' }
    ];
    const totalHours = games.reduce((sum, g) => sum + g.hours, 0);
    expect(totalHours).assertEqual(150);
  });
});

5.3 UI 测试

typescript 复制代码
// Ability.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { Driver, ON } from '@ohos.UiTest';

describe('GameAppUITest', () => {
  it('test_navigate_to_detail', 0, async () => {
    // 点击游戏卡片跳转到详情页
    const driver = await Driver.create();
    await driver.delay(1000);

    // 点击"最近游玩"区域的第一个游戏卡片
    const gameCard = await driver.findComponent(
      ON.text('艾尔登法环')
    );
    await gameCard.click();
    await driver.delay(500);

    // 验证是否跳转(检测详情页标题是否存在)
    const detailTitle = await driver.findComponent(
      ON.text('我的状态')
    );
    expect(detailTitle !== null).assertTrue();
  });
});

5.4 测试框架:Hypium + Hamock

项目使用 @ohos/hypium(单元测试框架)和 @ohos/hamock(Mock 框架):

复制代码
oh_modules/@ohos/
├── hypium/      # Hypium 测试框架
│   ├── index.ets
│   └── src/main/
└── hamock/      # Hamock Mock 框架
    └── index.ets

oh-package.json5 中的依赖声明:

json5 复制代码
{
  "dependencies": {
    "@ohos/hypium": "1.0.25",
    "@ohos/hamock": "1.0.0"
  }
}

六、ArkTS 严格模式最佳实践

6.1 常见规则与解法

规则 错误示例 正确写法
arkts-no-untyped-obj-literals { label: 'PC', count: 7 } 先定义接口,再赋值类型变量
arkts-no-noninferrable-arr-literals const arr = [1, 2, 3] const arr: number[] = [1, 2, 3]
arkts-no-for-of for (const g of games) for (let i: number = 0; ...)
arkts-strict-param-types .filter(g => g.status) .filter((g: Game) => g.status)

6.2 对象字面量模式

typescript 复制代码
// ❌ 错误:直接使用对象字面量
Column() {
  Text('通关')
    .backgroundColor(this.filter === '通关' ? '#FF6B35' : '#F0F0F0')
}

// ✅ 正确:将数组对象提取为独立类型变量
interface FilterItem {
  label: string;
  key: string;
}

const filterItems: FilterItem[] = [
  { label: '全部', key: 'all' },
  { label: '在玩', key: 'playing' }
];

ForEach(filterItems, (item: FilterItem) => {
  Text(item.label)
}, (item: FilterItem) => item.key)

6.3 数组字面量模式

typescript 复制代码
// ❌ 错误
const statuses = ['通关', '在玩', '想玩'];

// ✅ 正确
const statuses: string[] = ['通关', '在玩', '想玩'];

// ✅ 或者用类数组接口
const statuses: Array<string> = ['通关', '在玩', '想玩'];

6.4 ForEach key 生成规则

typescript 复制代码
// ✅ 筛选场景使用复合 key,确保重建
ForEach(
  this.getFilteredGames(),
  (game: GameItem) => { this.buildGameCard(game) },
  (game: GameItem) => game.id.toString() + this.filter
)

// ✅ 静态列表使用唯一 id
ForEach(
  this.filters,
  (f: string) => { Text(f) },
  (f: string) => f
)

七、DevEco Studio 调试技巧

7.1 hilog 日志输出

typescript 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0000;

// 调试日志
hilog.debug(DOMAIN, 'GameApp', 'Loading game id: %{public}d', gameId);

// 信息日志
hilog.info(DOMAIN, 'GameApp', 'Page loaded successfully');

// 错误日志
hilog.error(DOMAIN, 'GameApp', 'Failed to load game: %{public}s', errMsg);

7.2 性能优化建议

  1. 惰性加载 :避免在 aboutToAppear 中执行繁重计算
  2. ForEach key 优化:静态列表用稳定 key,动态列表用复合 key
  3. @Builder 粒度控制 :每个 @Builder 控制在 30-50 行内
  4. 减少嵌套层级 :避免 Stack > Column > Row > ... 过深嵌套

7.3 常见错误处理

typescript 复制代码
// 路由参数缺失的兜底
aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['gameId'] !== undefined) {
    this.gameId = params['gameId'] as number;
  }
  this.loadGame();
}

// 数据加载失败的回退
if (!this.game) {
  this.game = allGames[0];  // 默认显示第一个
}

八、项目总结

8.1 项目规模

维度 数据
页面数 5 个
总代码行 ~1800 行 ArkTS
数据模型 Game, GameItem, GameDetail, WishItem, GameStat, GenrePie
组件复用 15 个 @Builder 组件
路由跳转 7 条路由路径

8.2 技术亮点

  1. 纯 ArkTS 图表:不使用第三方库,用 Stack + Column 实现条形图、柱状图
  2. 色彩识别系统:每个游戏分配主题色,替代封面图,减少资源占用
  3. 条件渲染 :善用 if 判断和三元运算符实现动态 UI
  4. 响应式状态:@State 结合 ForEach,数据变化自动驱动 UI 更新

8.3 可扩展性方向

复制代码
当前实现              →    未来扩展
───────────────────────────────────────────
静态 mock 数据        →    接入网络 API
本地 state            →    AppStorage/LocalStorage
router 导航           →    Navigation 组件
纯色封面              →    网络图片加载
无状态持久化          →    Preferences/RelationalStore
单 entry 模块         →    multi-har/library 模块

九、结语

五篇博文,从项目搭建、列表开发、详情交互、数据统计到工程优化,我们完整走完了一个鸿蒙原生应用的开发全流程。

核心收获

  • 🏗️ Stage 模型 + ArkTS 的项目结构
  • 🎨 声明式 UI 的 @Builder + @State 组合
  • 🔄 路由传参与页面间通信
  • 📊 纯 UI 组件实现数据可视化
  • ⚡ 严格模式下的类型安全编程

鸿蒙生态正在快速发展,掌握 ArkTS 和 Stage 模型是当前鸿蒙开发的关键技能。希望这五篇实战文章能够帮助更多开发者顺利上手鸿蒙原生应用开发!


系列目录(全五篇)

  • 第一篇:项目搭建与首页开发
  • 第二篇:游戏库列表与筛选排序
  • 第三篇:游戏详情页与交互功能
  • 第四篇:愿望单与个人统计
  • 第五篇:路由导航与工程优化(本文)
    项目信息:基于 HarmonyOS API 23 (compatibleSdkVersion 23, targetSdkVersion 24) + Stage 模型 + ArkTS,使用 DevEco Studio 开发。所有代码均已通过 ArkTS 严格模式编译。
相关推荐
风满城332 小时前
【鸿蒙原生应用开发实战】第三篇:表单录入与详情展示——AddPetPage + PetDetailPage 完整实现
华为·harmonyos
风满城332 小时前
【鸿蒙原生应用开发实战】第一篇:从零搭建“萌宠日记“项目——Stage模型与工程架构解析
华为·harmonyos
charlee442 小时前
Unity项目适配华为鸿蒙系统的原生库加载问题排查与解决
华为·unity3d·鸿蒙·cmake·c/c++·relro
狼哥16863 小时前
《新闻资讯》二、公共能力层模块实现指南
ui·华为·harmonyos
Ww.xh3 小时前
启用Hypervisor解决模拟器问题
华为·harmonyos
金启攻4 小时前
【鸿蒙原生应用实战】第二篇:装备库页面——分类筛选与数据驱动UI
harmonyos
木咺吟6 小时前
鸿蒙原生应用实战(四):愿望单与个人统计 — 数据聚合与可视化
华为·harmonyos
木咺吟6 小时前
鸿蒙原生应用实战(二):游戏库列表与筛选排序 — 卡片式UI设计
harmonyos
互联网散修8 小时前
鸿蒙实战:从零实现自定义相机(下)——填平预览拉伸、比例错乱、缩略图消失的六大坑
数码相机·华为·harmonyos