【鸿蒙心迹】从踩坑到上线:一次 HarmonyOS 应用开发的完整实战复盘

1. 写在前面:为什么想记录这段开发经历
第一次真正把一个 HarmonyOS 应用从零做到上架,前后大概花了两个多月。这两个月里,我踩过的坑比过去一年做 Android 开发时还多——不是鸿蒙本身有多难,而是它的开发范式、工程结构、调试手段和生态工具链,和过去熟悉的路径确实有不少差异。
写这篇文章的初衷很简单:把自己真实踩过的坑、验证过的方案、以及最后跑通的代码整理出来,让后来者少走一点弯路。文章不追求大而全,只讲我亲手写过、跑过、上线过的东西。
本文覆盖的内容包括:工程初始化与目录结构、ArkTS 与状态管理实践、页面路由与多设备适配、网络请求与数据持久化、常见踩坑记录、上架审核注意事项,以及最后的项目复盘。全文约 3000 字,关键位置都附了可运行的代码片段。
2. 工程初始化:先把地基打对
2.1 环境与版本
我使用的是 DevEco Studio 5.0 系列,配套 API 12(HarmonyOS NEXT)。这里第一个坑就来了:API 版本和 SDK 版本必须严格对齐,否则会出现编译通过但运行时报 Cannot find module 的情况。
建议在 build-profile.json5 里明确锁定编译版本:
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.0.0(12)",
"runtimeOS": "HarmonyOS"
}
]
}
}
compatibleSdkVersion 决定了应用能运行的最低系统版本,compileSdkVersion 决定编译时使用的 API。两者不要混用,否则很容易出现「本地能跑、真机崩溃」的问题。
2.2 目录结构约定
HarmonyOS 的工程结构是 Stage 模型,核心目录如下:
entry/
├── src/main/
│ ├── ets/
│ │ ├── entryability/ # UIAbility 入口
│ │ ├── pages/ # 页面
│ │ ├── components/ # 自定义组件
│ │ ├── viewmodel/ # 状态与业务逻辑
│ │ ├── model/ # 数据模型
│ │ └── utils/ # 工具函数
│ ├── resources/ # 资源文件
│ └── module.json5 # 模块配置
我的经验是:尽早把 components、viewmodel、model 三层拆开。鸿蒙的 ArkUI 声明式写法很容易让人把所有逻辑都堆在页面里,前期图快,后期改一个字段要翻五个文件。
3. ArkTS 与状态管理:最容易翻车的地方
3.1 @State、@Prop、@Link 的边界
ArkTS 的状态装饰器是新手最容易混淆的部分。简单总结我踩坑后的理解:
@State:组件内部状态,自己维护、自己修改。@Prop:父组件传给子组件的单向副本,子组件改了不影响父组件。@Link:父子双向同步,子组件修改会回传父组件。@Observed+@ObjectLink:用于对象类型的深层观察。
一个典型错误是:把对象数组用 @State 声明,然后直接修改数组内元素的属性,发现 UI 不刷新。原因是 @State 只观察第一层引用变化。
@Observed
class TaskItem {
id: number = 0;
title: string = '';
done: boolean = false;
}
@Component
struct TaskRow {
@ObjectLink task: TaskItem;
build() {
Row() {
Text(this.task.title)
.decoration({ type: this.task.done ? TextDecorationType.LineThrough : TextDecorationType.None })
Toggle({ type: ToggleType.Checkbox, isOn: this.task.done })
.onChange((isOn: boolean) => {
this.task.done = isOn; // 配合 @Observed 才能触发刷新
})
}
}
}
3.2 状态管理选型建议
项目规模一旦超过 10 个页面,我强烈建议引入 AppStorage 或 LocalStorage 做跨页面共享,而不是层层传参。对于更复杂的场景,可以考虑官方推荐的 @ohos/arkui 状态管理方案或第三方状态库。
4. 页面路由与多设备适配
4.1 路由跳转
Stage 模型下推荐使用 router 或 Navigation。新项目我更推荐 Navigation,因为它天然支持分栏布局,对折叠屏、平板更友好。
import { router } from '@kit.ArkUI';
// 简单跳转
router.pushUrl({
url: 'pages/DetailPage',
params: { id: 1001 }
});
// 接收参数
const params = router.getParams() as Record<string, number>;
const id = params?.id;
4.2 折叠屏与多形态适配
这是本次活动的优先推荐方向之一,也是我实际花时间最多的部分。核心思路是用断点(breakpoint)驱动布局,而不是写死宽高。
@Entry
@Component
struct AdaptivePage {
@StorageProp('currentBreakpoint') breakpoint: string = 'sm';
build() {
if (this.breakpoint === 'lg') {
Row() {
SideBar()
ContentArea()
}.width('100%')
} else {
Column() {
ContentArea()
}.width('100%')
}
}
}
配合 GridRow / GridCol 的响应式栅格,可以比较优雅地处理手机、折叠屏展开态、平板三种形态。踩坑点在于:折叠屏展开/折叠会触发窗口尺寸变化,需要在 onWindowStageCreate 中监听并更新断点,否则布局不会自动切换。
5. 网络请求与数据持久化
5.1 网络请求封装
鸿蒙推荐使用 @kit.NetworkKit 中的 http 模块。我封装了一个统一的请求工具,处理超时、错误码和 Token 注入:
import { http } from '@kit.NetworkKit';
export async function request<T>(url: string, method: http.RequestMethod = http.RequestMethod.GET, extraData?: object): Promise<T> {
const httpRequest = http.createHttp();
try {
const resp = await httpRequest.request(url, {
method,
header: { 'Content-Type': 'application/json' },
extraData: extraData ? JSON.stringify(extraData) : undefined,
connectTimeout: 10000,
readTimeout: 10000
});
if (resp.responseCode === 200) {
return JSON.parse(resp.result as string) as T;
}
throw new Error(`HTTP ${resp.responseCode}`);
} finally {
httpRequest.destroy();
}
}
重要提醒:httpRequest 用完必须 destroy(),否则会泄漏连接。我在压测时就是因为忘了这一步,导致请求数一多就卡死。
5.2 数据持久化
轻量数据用 preferences,结构化数据用 relationalStore(关系型数据库)。我这里的场景是缓存用户配置,用 preferences 足够:
import { preferences } from '@kit.ArkData';
const store = await preferences.getPreferences(getContext(this), 'user_config');
await store.put('token', 'xxx');
await store.flush(); // 必须 flush 才会落盘
又一个坑:put 之后不调用 flush(),数据只在内存里,应用重启就丢了。
6. 踩坑记录:那些让我加班到深夜的问题
6.1 真机调试白屏
现象:模拟器正常,真机启动白屏。排查后发现是 module.json5 中 abilities 的 srcEntry 路径写错,导致 UIAbility 没被正确加载。建议每次改完配置都重新 Sync 一次。
6.2 图片资源不显示
resources/base/media 下的图片,引用时必须带扩展名且不能有中文名。我一开始用了 icon-首页.png,编译不报错但运行时不显示,改成 icon_home.png 后正常。
6.3 列表卡顿
List 组件数据量大时卡顿,原因是没用 LazyForEach。改成懒加载后,1000 条数据滚动流畅:
List() {
LazyForEach(this.dataSource, (item: TaskItem) => {
ListItem() {
TaskRow({ task: item })
}
}, (item: TaskItem) => item.id.toString())
}
6.4 上架审核被拒
第一次提审被拒,原因是隐私政策未在首次启动时弹窗告知,且申请了不必要的权限(定位)。整改方式:删除无用权限声明,补充隐私弹窗,二次提审通过。
7. 上架审核要点清单
结合我的实际经历,整理一份提审前自查清单:
- 隐私政策:首次启动必须弹窗,且需用户主动同意后才能初始化 SDK。
- 权限最小化:
module.json5中requestPermissions只声明真正用到的权限。 - 应用信息完整:图标、名称、版本号、简介不能为空或占位。
- 内容合规:无违规内容、无诱导分享、无外链跳转违规页面。
- 兼容性:至少在手机 + 折叠屏/平板各测一遍。
8. 项目复盘:如果重来一次
回头看,这个项目最大的收获不是学会了某个 API,而是理解了鸿蒙「一次开发、多端部署」的设计哲学。如果重来一次,我会做三件事:
- 第一天就搭好分层结构,而不是边写边重构。
- 尽早接入真机调试,模拟器的坑掩盖了太多真实问题。
- 把断点适配做成基础设施,而不是最后临时补。
鸿蒙生态还在快速演进,文档和 API 都在持续更新。保持关注官方 Release Notes,比死记硬背 API 更重要。
9. 结语
以上就是我这次鸿蒙开发实战的完整记录。从环境搭建、状态管理、多设备适配,到网络、持久化、踩坑与上架,每一步都是真实经历。希望这些内容对正在做鸿蒙开发的你有一点帮助。
如果你也在写鸿蒙开发笔记,欢迎一起交流。开发路上,踩过的坑都是财富。
更多推荐



所有评论(0)