鸿蒙原生应用实战(五):路由导航与工程优化 — 从开发到上线的完整流程
鸿蒙原生应用实战(五):路由导航与工程优化 — 从开发到上线的完整流程
一、前言
经过前四篇的开发,我们的游戏收藏夹 App 已经拥有 5 个页面、1800+ 行 ArkTS 代码。本篇将从架构高度重新审视整个项目,涵盖:
- 路由系统深度解析与导航架构设计
- module.json5 配置详解
- 构建配置与性能优化
- 单元测试与 UI 测试实践
- ArkTS 严格模式最佳实践
- DevEco Studio 调试技巧
二、路由系统深度解析
2.1 路由架构概览
Index (首页) / | \ ▼ ▼ ▼ GameListPage WishPage StatsPage | ▼ GameDetailPage整个 App 的路由关系是星型拓扑:首页作为 Hub,可以跳转到任意子页面,详情页作为最深层级页面。
2.2 路由配置
在main_pages.json中注册所有页面路由:
{"src":["pages/Index","pages/GameListPage","pages/GameDetailPage","pages/WishPage","pages/StatsPage"]}在module.json5中引用:
{"module":{"pages":"$profile:main_pages","abilities":[{"name":"EntryAbility","srcEntry":"./ets/entryability/EntryAbility.ets","launchType":"standard"}]}}2.3 路由 API 全面解析
2.3.1 路由导入
// API 23 下的正确导入方式importrouterfrom'@ohos.router';注意:API 23 版本下,
router从@ohos.router导入,而不是@kit.AbilityKit。这个版本@kit.AbilityKit不导出 router API,如果用错会导致编译错误。
2.3.2 页面跳转
// 不带参数的跳转router.pushUrl({url:'pages/GameListPage'});// 带参数的跳转router.pushUrl({url:'pages/GameDetailPage',params:{gameId:1}});2.3.3 接收参数
// 参数接收的标准写法constparams=router.getParams()asRecord<string,Object>;if(params&¶ms['filter']!==undefined){this.filter=params['filter']asstring;}2.3.4 返回上一页
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 完整配置解读
{ "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
{ "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
{ "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 构建命令
hvigorw--modemodule\-pmodule=entry@default\-pproduct=default\-prequiredDeviceType=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.ets5.2 本地单元测试
// LocalUnit.test.etsimport{describe,it,expect}from'@ohos/hypium';import{UIAbility}from'@kit.AbilityKit';describe('GameDataTest',()=>{it('test_filter_status',0,()=>{// 测试筛选逻辑的正确性constgames=[{id:1,title:'Game A',status:'通关'},{id:2,title:'Game B',status:'在玩'}];constfiltered=games.filter(g=>g.status==='通关');expect(filtered.length).assertEqual(1);expect(filtered[0].title).assertEqual('Game A');});it('test_calc_stats',0,()=>{// 测试统计数据计算constgames=[{id:1,hours:100,status:'通关'},{id:2,hours:50,status:'在玩'}];consttotalHours=games.reduce((sum,g)=>sum+g.hours,0);expect(totalHours).assertEqual(150);});});5.3 UI 测试
// Ability.test.etsimport{describe,it,expect}from'@ohos/hypium';import{Driver,ON}from'@ohos.UiTest';describe('GameAppUITest',()=>{it('test_navigate_to_detail',0,async()=>{// 点击游戏卡片跳转到详情页constdriver=awaitDriver.create();awaitdriver.delay(1000);// 点击"最近游玩"区域的第一个游戏卡片constgameCard=awaitdriver.findComponent(ON.text('艾尔登法环'));awaitgameCard.click();awaitdriver.delay(500);// 验证是否跳转(检测详情页标题是否存在)constdetailTitle=awaitdriver.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.etsoh-package.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 对象字面量模式
// ❌ 错误:直接使用对象字面量Column(){Text('通关').backgroundColor(this.filter==='通关'?'#FF6B35':'#F0F0F0')}// ✅ 正确:将数组对象提取为独立类型变量interfaceFilterItem{label:string;key:string;}constfilterItems:FilterItem[]=[{label:'全部',key:'all'},{label:'在玩',key:'playing'}];ForEach(filterItems,(item:FilterItem)=>{Text(item.label)},(item:FilterItem)=>item.key)6.3 数组字面量模式
// ❌ 错误conststatuses=['通关','在玩','想玩'];// ✅ 正确conststatuses:string[]=['通关','在玩','想玩'];// ✅ 或者用类数组接口conststatuses:Array<string>=['通关','在玩','想玩'];6.4 ForEach key 生成规则
// ✅ 筛选场景使用复合 key,确保重建ForEach(this.getFilteredGames(),(game:GameItem)=>{this.buildGameCard(game)},(game:GameItem)=>game.id.toString()+this.filter)// ✅ 静态列表使用唯一 idForEach(this.filters,(f:string)=>{Text(f)},(f:string)=>f)七、DevEco Studio 调试技巧
7.1 hilog 日志输出
import{hilog}from'@kit.PerformanceAnalysisKit';constDOMAIN=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 性能优化建议
- 惰性加载:避免在
aboutToAppear中执行繁重计算 - ForEach key 优化:静态列表用稳定 key,动态列表用复合 key
- @Builder 粒度控制:每个
@Builder控制在 30-50 行内 - 减少嵌套层级:避免
Stack > Column > Row > ...过深嵌套
7.3 常见错误处理
// 路由参数缺失的兜底aboutToAppear():void{constparams=router.getParams()asRecord<string,Object>;if(params&¶ms['gameId']!==undefined){this.gameId=params['gameId']asnumber;}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 技术亮点
- 纯 ArkTS 图表:不使用第三方库,用 Stack + Column 实现条形图、柱状图
- 色彩识别系统:每个游戏分配主题色,替代封面图,减少资源占用
- 条件渲染:善用
if判断和三元运算符实现动态 UI - 响应式状态:@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 严格模式编译。
