鸿蒙原生应用开发实战(五):地图可视化与性能优化------钓点地图与构建发布全攻略
前言
这是"钓点日记"开发系列的最终章 。本篇文章将完成最后一个核心页面------钓点地图,并系统总结鸿蒙应用的构建、调试和发布全流程。
本篇主要内容:
- 钓点地图(SpotsMapPage):自定义模拟地图与标记交互
- 项目完整架构回顾(8个页面全景)
- 构建配置详解(build-profile、hvigor)
- 性能优化最佳实践
- 真机调试与打包发布
一、钓点地图:自定义可视化实现
钓点地图用模拟地图 的方式展示钓点分布,通过 Stack 叠加和 position() 绝对定位实现。
1.1 为什么不用地图SDK?
在鸿蒙生态中,地图SDK集成方案还不成熟(需要对接华为Map Kit,配置复杂、应用包体增大)。因此我们选择 模拟地图 方案:
- 轻量:无需第三方SDK,零依赖
- 可控:数据完全本地化,UI自由定制
- 直观:抽象的地理信息展示,对钓鱼场景足够
- 可扩展:后续可无缝替换为真实地图
1.2 数据模型
typescript
interface MapSpot {
id: number;
name: string; // 钓点名称
xPct: number; // 在背景图中的X百分比位置
yPct: number; // 在背景图中的Y百分比位置
rating: number; // 评分
type: string; // 类型:水库/河流/湖泊/池塘/海钓
}
private spots: MapSpot[] = [
{ id: 1, name: '月亮湾水库', xPct: 25, yPct: 35, rating: 4, type: '水库' },
{ id: 2, name: '清溪河下游', xPct: 60, yPct: 20, rating: 5, type: '河流' },
{ id: 3, name: '龙潭湖', xPct: 75, yPct: 55, rating: 3, type: '湖泊' },
{ id: 4, name: '碧波潭', xPct: 40, yPct: 65, rating: 4, type: '水库' },
{ id: 5, name: '野塘', xPct: 15, yPct: 75, rating: 3, type: '池塘' },
{ id: 6, name: '金沙湾海滨', xPct: 85, yPct: 30, rating: 4, type: '海钓' }
];
为什么用百分比而不是绝对坐标?
- 百分比适配不同屏幕尺寸
- 修改地图背景图时无需调整坐标
- 更容易实现响应式布局
1.3 Stack 叠加布局
地图页面的核心是 Stack 容器。与其他布局不同,Stack 允许子组件叠加 和绝对定位:
typescript
Stack() {
// 背景层:网格 + 地形标注
Column() {
GridBackground()
TerrainLabels()
}
// 标记层:钓点标记
ForEach(this.spots, (spot: MapSpot) => {
SpotMarker({ spot: spot })
})
// 图例层
MapLegend()
}
.width('95%')
.height(500)
1.4 网格背景
我们使用两层 ForEach 生成5×5的网格参考线:
typescript
Column() {
ForEach([1, 2, 3, 4, 5], (row: number) => {
Row() {
ForEach([1, 2, 3, 4, 5], (col: number) => {
Text('.').fontSize(40).fontColor('#F0F0F0')
}, (col: number) => col.toString())
}
}, (row: number) => row.toString())
}
.padding(8)
虽然看起来只是输出一些点号,但配合 Column 和 Row 的约束,这些点构成了一个不可见的坐标网格,帮助用户感知空间布局。
1.5 地形标注
typescript
Text('🏔️ 西山').position({ x: '8%', y: '5%' })
Text('🌲 森林公园').position({ x: '55%', y: '8%' })
Text('🏙️ 市区').position({ x: '45%', y: '40%' })
Text('🌊 海湾').position({ x: '75%', y: '25%' })
Text('🛣️ G35高速').position({ x: '30%', y: '50%' })
position() 是 Stack 子组件的特有属性,设置相对于 Stack 容器的位置。
1.6 钓点标记与交互
每个钓点标记包含图标和名称:
typescript
ForEach(this.spots, (spot: MapSpot) => {
Column() {
Text('🎣').fontSize(24)
Text(spot.name).fontSize(11)
.backgroundColor(Color.White)
.padding({ left: 4, right: 4 }).borderRadius(4)
}
.position({ x: spot.xPct + '%', y: spot.yPct + '%' })
.onClick(() => {
this.selectedSpot = spot; // 选中钓点,弹出详情
})
}, (spot: MapSpot) => spot.id.toString())
交互流程:
- 用户点击 🎣 标记
this.selectedSpot = spot状态更新- 页面底部弹出选中钓点的信息卡片
- 点击"查看详情"跳转到钓点详情页
1.7 选中卡片弹出
typescript
@State selectedSpot: MapSpot | null = null;
if (this.selectedSpot) {
Column() {
Row() {
Text(this.selectedSpot!.name).fontSize(18).fontWeight(FontWeight.Bold)
Blank()
Text(this.selectedSpot!.type)
.fontColor(Color.White)
.backgroundColor(this.getSpotColor(this.selectedSpot!.type))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(10)
}
Row() {
Text('评分: ')
ForEach([1, 2, 3, 4, 5], (star) => {
Text(star <= this.selectedSpot!.rating ? '★' : '☆')
.fontSize(18).fontColor($r('app.color.rating_star'))
})
Blank()
Button('查看详情').onClick(() => {
router.pushUrl({
url: 'pages/SpotDetailPage',
params: { spotData: this.selectedSpot! }
})
})
}
.margin({ top: 8 })
}
}
注意非空断言 ! :由于 selectedSpot 类型为 MapSpot | null,在条件判断后使用需要 ! 告诉编译器该值一定不为 null。
1.8 类型颜色映射
不同类型的钓点用不同颜色标识:
typescript
getSpotColor(type: string): string {
if (type === '水库') return '#FF4A90D9'; // 蓝色
if (type === '河流') return '#FF4CAF50'; // 绿色
if (type === '湖泊') return '#FF2196F3'; // 浅蓝
if (type === '池塘') return '#FFFF9800'; // 橙色
if (type === '海钓') return '#FF0288D1'; // 深蓝
return '#FF9E9E9E'; // 默认灰色
}
1.9 图例
地图右下角的图例帮助用户理解颜色含义:
typescript
Row() {
Circle().width(8).height(8).fill('#FF4A90D9')
Text('水库').fontSize(10).margin({ right: 8, left: 2 })
Circle().width(8).height(8).fill('#FF4CAF50')
Text('河流').fontSize(10).margin({ right: 8, left: 2 })
Circle().width(8).height(8).fill('#FFFF9800')
Text('池塘').fontSize(10).margin({ right: 8, left: 2 })
Circle().width(8).height(8).fill('#FF0288D1')
Text('海钓').fontSize(10).margin({ left: 2 })
}
.position({ x: '5%', y: '90%' })
.backgroundColor('rgba(255,255,255,0.8)')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
二、项目架构全景回顾
至此,8个页面全部开发完成。让我们回顾完整的项目架构:
2.1 页面关系图
Index.ets ← 首页:天气+附近钓点列表+底部导航
│
├── SpotDetailPage.ets ← 钓点详情:参数接收+评分+评价
├── CatchRecordPage.ets ← 渔获记录:List列表+空状态
├── GearPage.ets ← 装备管理:分类渲染+状态标签
├── ProfilePage.ets ← 个人中心:统计卡片+@Prop子组件
├── FishEncyclopediaPage.ets ← 鱼种百科:搜索+分类+过滤
├── WeatherDetailPage.ets ← 天气详情:温度条+7天预报
└── SpotsMapPage.ets ← 钓点地图:Stack叠加+标记交互
2.2 路由注册(main_pages.json)
json
{
"src": [
"pages/Index",
"pages/SpotDetailPage",
"pages/CatchRecordPage",
"pages/GearPage",
"pages/ProfilePage",
"pages/FishEncyclopediaPage",
"pages/WeatherDetailPage",
"pages/SpotsMapPage"
]
}
2.3 资源清单
| 资源文件 | 内容 |
|---|---|
| AppScope/resources/string.json | 应用名 app_name |
| entry/string.json | 18个页面/功能字符串 |
| entry/color.json | 11个颜色定义(主题色+文字色+状态色) |
| entry/float.json | 8个尺寸定义(字号+间距+圆角) |
三、构建配置详解
3.1 项目级 build-profile.json5
json5
{
"app": {
"signingConfigs": [],
"compileSdkVersion": 23,
"compatibleSdkVersion": 23,
"products": [
{
"name": "default",
"signingConfig": "default"
}
]
}
}
compileSdkVersion:编译SDK版本(23对应API 23)compatibleSdkVersion:最低兼容版本
3.2 模块级 entry/build-profile.json5
json5
{
"apiType": "stageMode",
"buildOption": {
"strictMode": {
"arkts": {
"allowed": []
}
}
},
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
strictMode 配置控制 ArkTS 严格模式的规则。如果某些规则过于严格,可以在这里豁免。
3.3 hvigor 构建配置
hvigor/hvigor-config.json5 是构建工具的核心配置:
json5
{
"model": "stage",
"app": {
"compileSdkVersion": 23,
"compatibleSdkVersion": 23
}
}
3.4 oh-package.json5 包管理
json5
{
"name": "MyApplication",
"version": "1.0.0",
"dependencies": {
"@ohos/hamock": "^1.0.0",
"@ohos/hypium": "^1.0.25"
}
}
测试依赖 hamock(Mock框架)和 hypium(测试框架)是自动添加的。
四、性能优化最佳实践
4.1 列表性能优化
对比 List 和 Scroll + ForEach 的性能差异:
| 场景 | List + ListItem | Scroll + ForEach |
|---|---|---|
| 项数 ≤ 20 | 差异不大 | 差异不大 |
| 项数 20-100 | 复用优势明显 | 可能卡顿 |
| 项数 > 100 | 推荐使用 | 不推荐 |
| 复杂卡片 | 复用减少布局计算 | 每项独立布局 |
优化建议:
- 渔获记录页(4项)→ 简单场景,List够用
- 鱼种百科(8项)→ List + 键值优化
- 装备管理(5项)→ Scroll + ForEach 足够
4.2 @State 最小化原则
只把UI依赖的变量 声明为 @State:
typescript
// ✅ 正确:UI需要显示的变量
@State spots: FishingSpot[] = [];
@State searchQuery: string = '';
// ❌ 错误:不需要UI同步的变量
private categories: string[] = ['全部', '淡水鱼', '海水鱼', '路亚目标鱼'];
private fishList: FishInfo[] = [ /* 数据 */ ];
4.3 计算属性 vs 手动维护
typescript
// ✅ 推荐:使用 getter 自动计算
get filteredFish(): FishInfo[] {
// 依赖 @State 变量,自动重新计算
}
// ❌ 不推荐:手动维护过滤结果
@State filteredFish: FishInfo[] = [];
// 每次筛选条件变化都要手动调用 updateFilter()
4.4 ForEach 键值优化
typescript
// ✅ 好的key:稳定且唯一
(item: FishInfo) => item.id.toString()
// ⚠️ 可接受的key:索引(仅当顺序不变)
(item: FishInfo, index: number) => index.toString()
// ❌ 不好的key:每次变化的对象引用
(item: FishInfo) => Math.random().toString()
稳定的key让框架可以精确追踪每个列表项,只更新变化的部分。
4.5 避免不必要的重新渲染
typescript
// ✅ 条件渲染:互斥条件用 if-else
if (loading) {
LoadingComponent()
} else if (error) {
ErrorComponent()
} else {
ContentComponent()
}
// ❌ 不推荐:同时渲染再隐藏
LoadingComponent()
ErrorComponent() // 被hidden隐藏,但仍在组件树中
ContentComponent()
五、调试与运行
5.1 本地模拟器
DevEco Studio 内置了模拟器管理器:
- 打开 Device Manager
- 创建 Phone 模拟器(选择API 23镜像)
- 启动模拟器
- 点击运行按钮
5.2 真机调试
- 手机开启 开发者模式(设置 → 关于手机 → 连续点击版本号7次)
- 开启 USB调试
- 连接电脑,选择"文件传输"模式
- DevEco Studio 识别设备后,选择真机运行
5.3 命令行构建
对于CI/CD场景,可以使用命令行构建:
bash
# 使用DevEco内置的Node和hvigor
"D:\DevEco Studio\tools\node\node.exe" \
"D:\DevEco Studio\tools\hvigor\bin\hvigorw.js" \
--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:守护进程模式
六、签名与打包发布
6.1 生成签名证书
在 DevEco Studio 中:
- Build → Generate Key and CSR
- 填写证书信息(组织、地区等)
- 生成
.p12密钥文件和.csr请求文件 - 在 AppGallery Connect 申请发布证书
- 下载
.cer证书文件和.p7bProfile文件
6.2 配置签名
在 build-profile.json5 中配置签名信息:
json5
{
"app": {
"signingConfigs": [{
"name": "default",
"material": {
"certPath": "path/to/release.cer",
"keyPath": "path/to/release.p12",
"keyStorePath": "path/to/release.p7b",
"keyStorePassword": "your-password",
"keyAlias": "your-alias",
"keyPassword": "your-key-password"
}
}]
}
}
6.3 构建正式包
Build → Build HAP(s)/APP(s) → Build APP(s)
生成的 .app 包位于 build/outputs/app/ 目录,可直接上传到 AppGallery Connect 进行分发。

七、项目总结
7.1 成果回顾
经过五篇文章的开发,我们完成了一个完整的鸿蒙原生应用:
| 维度 | 数据 |
|---|---|
| 页面数量 | 8个 |
| 代码量 | 约1500行 ArkTS |
| 组件类型 | @Entry页面8个,@Component子组件1个 |
| 路由注册 | 8条路由 |
| 资源文件 | string/color/float 各1个 |
| 数据模型 | 7个接口类型 |
7.2 核心技术点
- Stage模型:Ability + WindowStage 架构
- ArkTS语法:@State状态管理、@Prop组件通信
- ArkUI组件:Column/Row/Scroll/List/Stack/ForEach
- 路由导航:router.pushUrl/router.back/参数传递
- 资源管理:$r() 引用、多资源文件
- 交互模式:搜索/筛选/评分/条件渲染
7.3 优化方向
展望未来,这个App还可以从以下方向优化:
- 数据持久化:使用 @ohos.data.preferences 或 @ohos.data.relationalStore 保存用户的渔获记录和收藏
- 网络请求:接入 @ohos.net.http 实现实时天气和钓点数据
- 地图集成:对接华为Map Kit实现真实地图
- 动画效果:添加页面转场动画、列表加载动画
- 深色模式:完善 dark/ 目录下的资源适配
写在最后
五篇连载到此结束。从环境搭建到8个页面全部完成,我们完整走了一遍鸿蒙原生应用的开发全流程。
回看整个过程,ArkTS的声明式语法让UI开发变得直觉化,@State状态管理简化了数据驱动的复杂性,Stage模型的清晰分层让架构设计有据可循。鸿蒙生态正在快速成熟,现在是入局的最好时机。
如果你从第一篇文章读到了这里,相信你已经具备了独立开发鸿蒙原生应用的能力。拿起键盘,开始你的第一个鸿蒙项目吧!🐟
项目源码 :基于 HarmonyOS API 23 + Stage模型 + ArkTS
作者 :AtomCode
系列目录:
- 第一篇:项目初始化与环境配置
- 第二篇:首页与钓点列表开发
- 第三篇:数据管理与多页面交互
- 第四篇:复杂页面与交互体验
- 第五篇:地图可视化与性能优化(本篇,完结🎉)