
三个多月前我接到一个内部评估任务下一个跨端产品到底要不要基于 HarmonyOS_SDK 做一套原生实现还是继续沿用安卓包的简单适配。团队里没有一个人正经写过 HarmonyOS 代码最初的共识非常一致——这不就是套壳安卓吗让 Android 组的人顺手适配一下就行。说实话当时的我也是这么想的。直到我把官方 IDE 装好、创建了一个空白 Ability 工程、把第一个页面跑起来才发现这个判断错得离谱。HarmonyOS SDK 并不是 Android SDK 的换皮。Ability 模型、ArkUI 声明式语法、Stage 规范、权限体系和打包链路跟安卓的开发心智几乎没有重叠反倒更像是一套用前端声明式思路重新设计过的移动开发体系。这篇文章以我这三个月的实战记录为主线把环境搭建、工程初始化、核心架构理解、页面开发、权限与打包这些环节逐个拆开来讲重点写官方文档不会主动告诉你的差异和坑位。如果你正准备做技术预研或者已经被派去接手鸿蒙原生改造任务这篇文章应该能帮你省下至少两周的摸索时间。1. 技术选型预研为什么我最终倾向原生实现1.1 触发评估的产品背景我们下一期的产品规划覆盖手机、平板以及两个硬件合作伙伴的屏幕设备。按照老思路方案无非两条拿 Android APK 做机型适配等兼容层成熟或者引入跨端框架写一版 UI期望一次编译、多端运行。这两条路都有隐患安卓包在非安卓设备上的系统 API 行为一致性没人敢打保票跨端框架对系统能力的封装深度又不够特别是分布式数据、硬件协同这类鸿蒙侧重点强调的能力第三方框架大多只做到了 UI 层对齐。于是老板把评估任务交给了我这个新产品负责人核心问题只有一个HarmonyOS SDK 能不能撑起新增原生端的底座。初期我并没有抱太大期望毕竟规划里没有给它留太多预算和时间我的第一反应是先写一份 API 对比报告交差。但这个想法在我真正动手之后被彻底改写了。1.2 颠覆认知的三个瞬间第一击是 Ability 模型。安卓是 Activity Fragment Intent 那一套鸿蒙原生是 UIAbility WindowStage AbilityContext应用入口、窗口、上下文的关系完全不同。第二击是 UI 写法。ArkUI 是强约束的声明式语法页面描述与状态绑定你看不到 findViewById 那样的路径所有变化都由框架自动同步到视图开发者只负责改状态、描述结构。第三击是权限机制动态授权的触发时机、申请流程跟安卓并非一一对应API 也完全不同。这三个瞬间让我意识到拿安卓经验强行迁移是活不下去的。最终我把预算从适配一版改成了做一次完整原型验证也才有了后面这篇文章里的这些一手记录。1.3 适合哪些人读根据我的观察三类人最适合读这篇内容一是正在做技术选型的产品技术负责人二是从 Android/iOS 转过来写鸿蒙原生的开发者三是有前端经验、想快速切入声明式 UI 开发的程序员。前两类人建议重点看第 2 和第 3 部分因为工程搭建和生命周期是最容易劝退的门槛第三类人可以直接从第 4 部分的页面实战开始ArkUI 对前端背景的同学友好程度超出预期。2. 环境准备与第一个工程三个环节的连环坑2.1 工具链构成与前置配置HarmonyOS SDK 的开发工具链由三块组成官方 IDE、SDK Manager 和命令行辅助工具。SDK Manager 负责管理不同 API 版本的 SDK 组件从 API 9 到 API 12 我都接触过实测下来 API 12 的声明式能力最完整但部分第三方库对它的适配还停留在 API 10 的水平所以选型时不能只盯着最新版本还要看依赖生态的跟进速度。命令行工具常用的有两个ohpm 类似前端的 npm负责三方依赖安装hvigor 负责工程构建。这两个工具平时隐藏在 IDE 内部但排查构建问题的时候经常需要直接手敲命令建议提前把它们的安装位置和版本号确认好省得到时候两眼一抹黑。提示安装 IDE 和企业 SDK 时不要放到带空格的路径下。hvigor 在处理资源文件时对路径相当敏感这个莫名报错我排查了整整一个下午。另外SDK Manager 下载组件时对网络稳定性要求比较高。如果你的开发环境和组件仓库不在同一个网络区域建议提前把镜像源配置好等下载到一半断掉再重新拉取会浪费大量时间。2.2 工程目录结构第一个认知门槛新建 Empty Ability 工程之后顶层会有 AppScope 和 entry 两个目录。AppScope 放应用级配置entry 是唯一的模块目录所有源码都集中在 entry/src/main 下其中 ets 目录放页面代码resources 目录放字符串、颜色、媒体资源。最容易忽略的是 entry/src/main/module.json5这份文件相当于安卓的 Manifest主入口 Ability、权限声明、设备类型支持全部写在这里。我在第一个工程里犯过的最蠢错误是把页面目录建在了 ets 外面导致 IDE 找不到页面而错误提示只给了一个含糊的编译失败。这种问题排查起来非常费劲因为日志里根本不会告诉你目录结构不对只能自己对照官方模板找差异。建议新建工程后先花十分钟把目录树捋一遍搞清楚每个目录的语义再动手加代码。2.3 签名、模拟器与真机的三角关系HarmonyOS 工程默认不允许直接跑真机必须先配置签名。自动签名需要登录厂商开发者账号IDE 会自动生成调试证书手动签名则需要准备证书文件并配置证书链。第一次跑真机时最常见的坑是证书过期时间配置不正确导致安装失败我的建议是调试阶段直接使用自动签名别在证书上消耗精力。调试路径上有三个选择Previewer、模拟器和真机。Previewer 启动最快适合单页面的 UI 快速验证但它不支持全部系统能力某些网络接口在 Previewer 上会直接抛异常模拟器适合验证多页面跳转和大部分系统 API真机才是最终标准。我的习惯是写 UI 用 Previewer跑主流程用模拟器发版前完整过一遍真机。2.4 跑通第一个 Hello World 的正常流程新建 Empty Ability 工程等待依赖同步完成选择 Previewer 启动看到默认页面后再尝试修改一行文本、观察热刷新是否生效。这里有一个很多人会忽略的要点如果热刷新没生效优先去翻 IDE 底部的编译输出因为 ArkTS 的编译期检查极其严格一个类型问题就会阻塞整次刷新而界面表现看起来只是没变化。先把编译错误清零再谈界面优化。3. Ability 与页面生命周期被低估的核心门槛3.1 从 Activity 思维迁移到 UIAbility 思维安卓老手熟记的 Activity 生命周期是 onCreate、onResume、onPause 这套而 Stage 模型下 UIAbility 的核心生命周期是 onCreate、onWindowStageCreate、onForeground、onBackground、onDestroy。一个关键差异在于UIAbility 的 onCreate 并不代表界面已经可见它只是能力对象创建完成真正的界面加载发生在 onWindowStageCreate 里。我第一次调 UI 不刷新就是因为试图在 onWindowStageCreate 之前操作窗口对象。正确的习惯是onCreate 阶段只做进程级初始化例如创建单例、读取全局配置把界面相关的逻辑留给窗口阶段。下面是我在项目里实际使用的骨架代码export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { // 这里不要碰窗口和 UI只做进程级初始化 hilog.info(0x0000, EntryAbility, Ability created); } onWindowStageCreate(windowStage: window.WindowStage): void { // 窗口创建完成此时才可以加载页面 windowStage.loadContent(pages/Index); } onForeground(): void { // 应用切到前台 } onBackground(): void { // 应用退到后台 } onDestroy(): void { // 释放资源 } }3.2 窗口、页面、能力三者的关系用一个生活化的类比来解释这三者UIAbility 是餐厅的营业执照WindowStage 是营业中的店面页面是店面里的具体摊位。执照批下来onCreate不代表店面已经开张只有窗口创建成功onWindowStageCreate之后页面才有机会被加载。所以凡是依赖 UI 的初始化逻辑都不该放在 onCreate 里而应放在 onWindowStageCreate 或者更靠后的生命周期。理解了这层关系之后很多为什么我的页面数据不对为什么我拿不到窗口对象之类的问题就迎刃而解了。这个部分也是团队里安卓老手最容易转不过来的地方建议在写代码之前先花时间把这套模型在脑子里过一遍。3.3 路由方案router 与 Navigation 的取舍页面跳转方面HarmonyOS SDK 提供了两套方案。router 模块提供 pushUrl、back、replace 这类命令式 API简单直接适合页面层级浅的简单应用Navigation 是组件化方案通过 NavPathStack 管理页面栈适合底部 Tab 多、页面嵌套深的复杂应用。实际评估之后我的建议是原型和内部工具用 router 完全足够商业产品直接上 Navigation。因为在 Navigation 体系里系统返回手势、转场动画、跨层传参都能统一处理后续维护成本低很多。这里有一个常见坑router.pushUrl 的 url 必须和 pages 目录下的文件名完全对应大小写差一个字母就找不到目标页面而且错误要等到运行期才会暴露非常隐蔽。4. ArkUI 实战一个待办页面从 0 到 14.1 先拆页面结构再写代码我习惯先做组件树拆解再动手写界面。一个待办事项页面从上到下依次是标题区、输入区TextInput Button、列表区List ForEach。这种拆法在 ArkUI 下天然对应 Column 容器的嵌套每一层职责清晰。实际代码是这样的Entry Component struct ToDoPage { State tasks: string[] []; State inputText: string ; build() { Column({ space: 12 }) { Text(我的待办) .fontSize(24) .fontWeight(FontWeight.Bold) Row({ space: 8 }) { TextInput({ placeholder: 输入待办内容 }) .layoutWeight(1) .onChange((value: string) { this.inputText value; }) Button(添加) .onClick(() { const text this.inputText.trim(); if (text.length 0) { this.tasks.push(text); this.inputText ; } }) } List({ space: 8 }) { ForEach( this.tasks, (task: string) { ListItem() { Row() { Text(task).layoutWeight(1) Button(删除) .onClick(() { const index this.tasks.indexOf(task); if (index 0) { this.tasks.splice(index, 1); } }) } } }, (task: string) task ) } .layoutWeight(1) } .padding(16) .width(100%) .height(100%) } }这段代码里有几个细节值得展开Column 默认按内容高度排布列表区必须用 layoutWeight(1) 撑满剩余空间否则 List 根本不会滚动ForEach 的第三个参数是 key 生成器这里直接用任务文本做 key能保证新增和删除时列表只更新变化的条目否则会退化成整个列表重建性能和体验都会受影响。4.2 状态管理装饰器怎么选ArkUI 的响应式核心是状态装饰器。我高频使用的有五个State 管理组件内部状态变化触发 re-renderProp 接收父组件传入的只读值适合子组件展示场景Link 与父组件双向绑定适合需要双向同步的父子状态Observed 配合 ObjectLink 用于嵌套对象的深度监听Watch 则是在状态变化后触发回调适合做联动逻辑。新手最容易犯的错误是把子组件需要展示的数据也声明成 State结果父组件改了值子组件还抱着旧状态看起来就是UI 不刷新。正确的姿势是父组件持有唯一数据源子组件需要的展示数据用 Prop 传需要回传改动时用 Link 或者回调函数。这个原则和 React 的单一数据源思想是一致的前端同学接受起来非常快。4.3 列表项变成对象以后Observed 的正确用法实际业务里待办项往往不止是字符串还有完成状态、创建时间这些字段。数据结构升级成对象之后直接 push 一个对象进数组再修改对象属性UI 不一定感知得到。这时一般要给数据类加上 Observed 装饰器子组件里用 ObjectLink 接收并且要保证修改方式能被框架追踪。这块是 ArkUI 状态管理中最容易绕晕的部分。我的建议是从一开始就按类 Observed来组织数据模型别走字符串捷径。等业务量起来之后再回头改造数据模型成本会远高于一开始就按规范写。4.4 数据持久化Preferences 与关系型存储页面代码只维护内存态应用重启数据就丢了。轻量级数据我推荐使用 PreferencesAPI 风格接近安卓的 SharedPreferences适合存用户配置、首选项这类键值数据。基本流程是先通过 getPreferencesSync 拿到实例再 put 数据最后 flush 落盘。import { preferences } from kit.ArkData; let prefs preferences.getPreferencesSync(context, { name: todoStore }); prefs.putSync(lastCount, this.tasks.length); prefs.flush();我在这里提醒一句数据量一旦超过几百条、或者需要排序过滤查询就老实上 relationalStore 关系型存储使用体验接近简化版 SQLite。别图省事把列表数据也塞进 Preferences后面做分页和筛选的时候你会非常痛苦。4.5 日志与调试技巧开发阶段我习惯在关键位置加 hilog 日志但注意日志级别一定要分级错误用 error、调试用 debug别全用 info否则刷屏会把你真正想看的错误信息冲掉。UI 不更新的问题90% 出在装饰器加错或者数组操作方式不对上。ArkUI 能感知 push、splice 这类操作但如果你先用临时变量整体替换数组再赋值给 State某些场景下反而会丢响应这一点和 Vue2 的数组响应式限制有些相似。5. 权限申请、打包上架与三个高频翻车点5.1 运行时权限申请的正确姿势HarmonyOS 的权限分为系统授权和用户授权两类。用户授权类权限必须在运行时向用户申请我的做法分三步先在 module.json5 的 requestPermissions 里声明具体权限然后通过 abilityAccessCtrl.createAtManager() 拿到权限管理实例最后在合适的时机用 requestPermissionsFromUser 发起弹窗申请。关于申请时机我踩过的坑是过早触发。在 Ability 的 onCreate 里发起申请用户授权完回到页面时数据链路还没准备好容易造成状态丢失。更好的做法是等页面完成首帧渲染之后再请求在授权回调里做后续处理这样用户体验和数据流都更自然。5.2 HAP 打包与签名校验编译产物是 HAP 文件在官方 IDE 里执行构建命令就能生成。打包本身不复杂最折腾的是签名校验。我遇到的实际问题是 Debug 签名和 Release 签名的证书指纹不一致导致安装时直接提示签名校验失败。解决方法是生成正式证书链之后在 build-profile.json5 里把自动签名切换到手动签名再重新打一次包。另外要留意 targetSdkVersion 和 compatibleSdkVersion 这两个配置。compatible 决定应用能安装的最低系统版本target 决定应用能使用的新 API 能力两者差距过大的时候要么低版本设备装不上要么高版本系统上行为表现不一致发版前一定反复核对。5.3 高频翻车点汇总我把预研阶段收集到的最典型问题整理成了一张表方便你对照排查问题现象根本原因处理建议编译提示 ArkTS 语法错误禁用了部分 TS 宽松写法关掉 any、按严格模式写代码真机安装失败提示签名不对Debug 与 Release 证书不一致统一证书链检查 build-profilePreviewer 里接口调用失败预览器不支持部分系统能力UI 用 Previewer能力用模拟器或真机列表超过二百条滚动掉帧ForEach 全量创建列表项换 LazyForEach 延迟创建路由跳转找不到页面URL 大小写或路径不对核对 pages 目录文件名ArkTS 的限制值得单独拿出来说。它的类型检查比 TypeScript 严格不少any、非严格空值这些写法都会被编译器直接拦下来。从 JS/TS 转过来的人前期会很不适应我的经验是让编译器多报几次错比看十遍规范文档都管用。还有一个细节ArkTS 禁止用 any 做类型标注对象字面量必须显式声明接口这是不少前端同事转过来的第一道坎。5.4 性能优化的初步印象预研阶段我做了两个简单的性能测试页面冷启动和长列表滚动。冷启动的主要瓶颈在首帧渲染之前的初始化逻辑上优化思路是把非必要的初始化全部延迟到 onForeground 之后再做。长列表滚动则要借助 LazyForEach 延迟创建列表项普通 ForEach 在列表超过两百条时的帧率下降非常明显。相比跨端框架原生方案的优势在于可控性更强资源调度完全掌握在自己手里。6. 收尾前分享几点实操体会预研收尾时我们沉淀了一个内部评估模板后来几个项目组复用下来都觉得顺手第一周搭出一个跨 2 到 3 个页面的完整原型验证基础 UI 和生命周期第二周把权限、存储、网络这三类高频能力全部过一遍第三周专门做真机测试和性能摸底。按这个节奏走你得到的结论会比任何文档都可靠。我个人最深的体会是HarmonyOS SDK 并不是安卓的替代品而是一套重新设计过的移动开发体系。它的 Ability 模型和 ArkUI 声明式语法有明确的学习门槛但只要你愿意接受这套建模方式——Ability 管理生命周期、ArkUI 管理视图状态、编译期严格约束——写起来并不比安卓复杂甚至在一些多设备协同的痛点上天然占优势。团队里如果有前端背景的同事建议优先让他参与 UI 层开发ArkUI 的声明式语法对他们非常友好这可能是整个团队切入鸿蒙原生最平滑的一步。