我从一个很朴素的念头开始做这个项目:家里小孩上小学之后,课程表、兴趣班、作业截止时间全堆在一起,口头提醒经常漏,纸质的又容易丢。于是我用鸿蒙原生技术栈给自家孩子做了一个“宝贝日程表”,从新建工程到最后在应用市场上架,全程尽量少点鼠标,把所有能脚本化的步骤都交给我本地的命令行工具——也就是常说的 DevEco CLI 这套工具链。整个过程踩了不少坑,也把一套比较顺的链路跑通了,今天完整记录下来,给准备做鸿蒙 App、尤其是想走命令行工程化和上架流程的朋友做个参考。
这套内容适合两类人:一类是刚接触鸿蒙开发,想搞明白一个 App 从零到上架到底要经过哪些环节的新手;另一类是已经在用 DevEco Studio 写界面、但还没系统梳理过构建、签名、发布流程的开发者。我会把选型思路、核心代码、命令、踩坑记录都放出来,尽量做到你照着做也能跑通。
1. 项目概述与整体设计思路
1.1 宝贝日程表到底要做成什么样
认真想需求之前,我先给这个 App 定了三条产品底线。
第一,必须快。孩子打开 App 到看到今天要做什么,三秒内要完成,所以首页不能有复杂的加载动画和数据请求,所有数据优先本地化。
第二,必须直观。小学生认字有限,界面不能堆文字,要多用颜色、图标和大按钮。我最后把日程按“学习、运动、休息、兴趣班”四类做了四套配色,首页用时间轴卡片展示,一眼就能看清上午下午分别有什么安排。
第三,提醒要可靠。日程类 App 最核心的能力不是“记录”,而是“到点提醒”。所以我专门用了系统的后台代理提醒能力来做通知,而不是自己在前台跑定时器,这样才能保证 App 退到后台甚至被清理后,到点了依然能收到系统级通知。
功能上最终收敛成四个模块:今日日程时间轴、日程新增与编辑、按日期切换的日历视图、家长设置区。家长设置区里做了简单密码锁和提醒开关,用来防止小孩自己乱改日程或关掉通知。
1.2 命令行工具链的选型考量
做之前我其实犹豫过:直接开 DevEco Studio 不就行了?为什么还要折腾命令行?
我的判断是这样的:单机开发一个 demo,IDE 完全够用;但一旦牵扯到“可上架”“可交接”“可自动构建”这三个词,命令行工具链就是绕不开的。原因有三点。
第一,IDE 的构建过程本质上是把命令行工具包了一层壳。你用 Studio 点一次“Build”,底层跑的还是 hvigorw;点一次“Sync”,底层跑的是 ohpm install。既然绕不开,不如直接掌握它,遇到 IDE 缓存导致的各种诡异报错时,反而好排查。
第二,命令行天然适合自动化。我这次要反复打 debug 包、签名、检查产物,把这些步骤写成 shell 脚本后,每次打包只需要执行一条命令,中间不会有人为漏步骤的情况。后面如果要接流水线做持续集成,这套命令也一样能直接搬过去。
第三,DevEco CLI 这套工具链是跨平台可用的。我平时在 macOS 上写代码,但偶尔需要在 Linux 机器上出包,命令行环境迁移成本几乎为零,IDE 反而还要处理图形界面和授权问题。
当然,命令行不是万能的。UI 布局的实时预览、可视化调试这些,最终还是得回到 DevEco Studio 里做。我的做法是:写 UI 时用 IDE,跑构建、搞依赖、出签名包时切回命令行。两者配合,效率最高。
2. 环境准备与工程初始化
2.1 DevEco CLI 工具链构成
在动手之前,先把“DevEco CLI”到底包含哪些命令搞清楚。很多人以为它就是一个命令,实际上是一套工具链,我这次实际用到的有四个:
ohpm:鸿蒙的包管理器,类似前端的 npm,负责拉取三方库和工程依赖。hvigorw:构建工具,类似 Gradle,负责把 ArkTS 源码、资源文件编译打包成 HAP 包。hdc:设备调试工具,类似 ADB,负责连接真机、模拟器,安装和卸载 App、抓日志。hap-sign-tool.jar:签名工具,Java 编写的 jar 包,负责给 HAP 包做签名,也是上架前必经的一步。
这四个工具在安装 DevEco Studio 时一般都会带上,也可以单独下载 Command Line Tools 包,具体路径取决于你的安装方式。我的建议是:安装完成后,把这个几个工具的路径加进 shell 的PATH环境变量,后面用起来会顺手很多。
# macOS 环境变量配置示例 export DEVECO_SDK_HOME=$HOME/Library/Huawei/Sdk export PATH="$PATH:$DEVECO_SDK_HOME/command-line-tools/bin"配置完执行ohpm -v和hvigorw -v,能正常输出版本号就说明环境没问题。
2.2 从空文件夹到可构建工程
用命令行创建一个全新的鸿蒙工程,不像 IDE 里有“新建 Project 向导”那么可视化。我的做法是复制模板工程再改造,这是目前命令行场景下最稳妥的方式。
先从一个已有的标准工程目录开始,把entry模块和build-profile.json5这些骨架文件保留,然后统一改三个地方。
第一是AppScope/app.json5,里面的bundleName是整个应用的唯一标识,上架之后不能随便改。我这次定的是com.example.babyschedule,虽然示例域名不推荐商用,但自己学习测试够用。
{ "app": { "bundleName": "com.example.babyschedule", "vendor": "example", "versionCode": 1000000, "versionName": "1.0.0", "icon": "$media:app_icon", "label": "$string:app_name" } }第二是entry/src/main/module.json5,配置模块的基本信息、入口页面和需要的权限。
{ "module": { "name": "entry", "type": "entry", "srcEntrance": "./ets/entryability/EntryAbility.ets", "requestPermissions": [ { "name": "ohos.permission.PUBLISH_AGENT_REMINDER" } ] } }第三是工程根目录下的oh-package.json5,里面声明依赖。这里要特别说明一下,鸿蒙的依赖仓库源默认是华为的仓库,如果ohpm install很慢或者超时,可以手动配置镜像源。
ohpm config set registry https://repo.harmonyos.com/ohpm/ ohpm install依赖拉取完成,hvigorw就会自动执行相关任务。我先跑一个最基础的构建命令验证工程是否正常:
hvigorw assembleHap --mode module -p product=default看到 BUILD SUCCESSFUL 之后,说明这个空工程已经能出包了。接下来就可以开始写业务代码。
3. 核心功能开发:数据、界面与提醒
3.1 数据持久化设计
日程数据第一版我用了首选项来存,简单是简单,但很快发现不靠谱——首选项本质上是键值对存储,不适合做需要按时间范围查询的列表数据。比如“查出 3 月 1 日到 3 月 7 日所有日程”这种操作,用首选项要么全量读出来再遍历过滤,要么用多个 key 拼接,写起来很别扭,性能也差。
所以第二版我改成了关系型数据库。鸿蒙的@ohos.data.relationalStore提供了完整的 SQLite 能力,建表、增删改查都很顺畅。
我定义的表结构很简单,但足够覆盖核心场景:
CREATE TABLE IF NOT EXISTS schedule ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, category INTEGER NOT NULL, start_time INTEGER NOT NULL, end_time INTEGER NOT NULL, repeat_type INTEGER DEFAULT 0, remind_switch INTEGER DEFAULT 1, note TEXT );字段说明:category用来区分学习、运动、休息、兴趣班四类,对应不同的界面配色;start_time和end_time存毫秒级时间戳;repeat_type是重复规则,0 表示不重复,1 表示每天重复,2 表示每周重复,为后面做课程表循环日程留了扩展空间。
在实际封装时,我建了一个ScheduleDatabase单例类,把建库、建表、增删改查都封装成异步方法。比如添加一条日程:
async addSchedule(item: ScheduleItem): Promise<number> { const store = await this.getStore(); const values = new relationalStore.ValuesBucket(); values['title'] = item.title; values['category'] = item.category; values['start_time'] = item.startTime; values['end_time'] = item.endTime; values['repeat_type'] = item.repeatType; values['remind_switch'] = item.remindSwitch ? 1 : 0; values['note'] = item.note; const rowId = await store.insert('schedule', values); return rowId; }这里有个经验值得说一下:数据库初始化一定要放在应用启动早期完成,但建表操作不能阻塞主线程。我是在EntryAbility的onWindowStageCreate回调里异步初始化数据库,首页加载数据前先确认 ready 标志位,避免出现“界面出来了、数据还没准备好”的空白屏问题。
3.2 ArkUI 页面与状态管理
鸿蒙应用页面开发现在主推 ArkTS 和 ArkUI 声明式写法,跟 Flutter 或者 SwiftUI 的体验有点像。核心思想是:你声明界面长什么样,数据变了界面自动刷新。
首页我设计成上下两个区域:顶部是一个横向滚动的日期选择条,下面是当天日程的按时间排序列表。核心代码如下:
@Entry @Component struct HomePage { @State selectedDate: number = Date.now(); @State scheduleList: ScheduleItem[] = []; build() { Column() { DateBar({ selectedDate: this.selectedDate, onDateChange: (date) => this.onDateChange(date) }) List({ space: 12 }) { ForEach(this.scheduleList, (item: ScheduleItem) => { ListItem() { ScheduleCard({ item: item }) } }, (item: ScheduleItem) => item.id.toString()) } .layoutWeight(1) } } }@State是 ArkUI 的状态管理装饰器,变量变化时,依赖它的 UI 会自动重新渲染。我用@State持有当前选中日期和列表数据,切换日期时重新查数据库并赋值,界面就会自动更新,不需要手动操作 DOM。
这里给新手一个建议:列表项一定要给ForEach提供稳定的 key,我用的是日程 id。如果不给或者用数组下标当 key,刷新时很容易出现组件复用错乱的问题,表现为列表项内容串位,排查起来特别费劲。
日程卡片用了Card组件,卡片左侧有一条 6dp 宽的颜色条,用category映射四种颜色,这样孩子扫一眼颜色就知道是什么类型的安排。整个页面字体调大、按钮调圆,基本就是给儿童使用的交互偏好。
3.3 后台代理提醒的实现与避坑
这是整个项目里技术含量最高、也最容易翻车的地方。我一开始试图在页面里用setInterval定时检查当前时间,但很快发现这个方案根本不可靠:App 退到后台后,系统随时可能挂起定时器,更别说用户主动从最近任务里划掉了。
后来查文档发现鸿蒙提供了后台代理提醒能力,也就是把提醒任务交给系统,由系统进程在指定时间弹出通知。这才是日程提醒类 App 该有的姿势。
核心代码长这样:
import reminderAgentManager from '@ohos.reminderAgentManager'; async function createScheduleReminder(item: ScheduleItem): Promise<number> { const reminder: reminderAgentManager.ReminderRequestAlarm = { reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_ALARM, hour: new Date(item.startTime).getHours(), minute: new Date(item.startTime).getMinutes(), daysOfWeek: [], title: '宝贝日程提醒', content: item.title, notificationContent: { title: '宝贝日程提醒', content: item.title }, ringDuration: 5, snoozeTimes: 2, timeInterval: 10 }; const reminderId = await reminderAgentManager.publishReminder(reminder); return reminderId; }需要注意的是,要使用这个能力,必须在module.json5里申请ohos.permission.PUBLISH_AGENT_REMINDER权限,而且这个权限属于常规权限,不需要弹窗授权,只要声明了就能用。
这段代码里有三个我踩过的坑,必须单独写出来。
第一个坑是重复提醒。如果用户手滑点了一次“保存”,我往数据库插了一条数据,又往系统里注册了一个 reminder,那用户就会收到两条一模一样的通知。正确做法是数据库里专门存一列reminder_id,每次新增或修改日程时,先把旧提醒删掉再注册新的。核心就是先删后建,保证系统里的提醒与数据库记录一一对应。
第二个坑是提醒不响。真机测试时我发现,如果安装的是 debug 包,且应用被用户手动“停止”过,publishReminder的提醒就不会触发。后来排查文档才明白,应用被强制停止后,系统会把它标记为“已停止”状态,直到用户再次主动打开 App,后台代理提醒才会恢复。这是系统机制,不是代码 bug,但是测试时容易吓一跳。
第三个坑是时区问题。ReminderRequestAlarm的 hour 和 minute 是按设备当前时区算的。如果用户改过系统时区或者有跨时区出差场景,日程显示的时间和提醒触发时间可能对不上。稳妥的做法是注册提醒前,先用系统 API 把时间戳转成设备本地时区的小时和分钟。
4. 构建、签名与真机联调
4.1 hvigorw 打包 HAP
代码写完,核心功能自测通过,接下来就是把工程构建成可以安装和上架的 HAP 包。
鸿蒙工程构建的本质是hvigorw根据build-profile.json5里的配置,把 ArkTS 编译成方舟字节码,再和资源文件一起打包。构建产物目录通常在entry/build/default/outputs/default/下面,能找到一个.hap文件。
debug 和 release 两种构建方式的区别主要在签名和混淆上。日常调试可以直接构建 debug 包,用 debug 证书签名,安装到真机跑;上架前必须构建 release 包,用 release 证书签名,同时建议开启混淆。
# 构建 release 包 hvigorw assembleHap --mode module -p product=default --no-daemon加上--no-daemon是为了让构建在前台跑完就退出,适合脚本场景。构建完成后,我先检查一下 HAP 包大小:
ls -lh entry/build/default/outputs/default/一个带图片资源的日程 App,HAP 体积一般会在几 MB 到十几 MB 之间。如果发现体积异常大,多半是 assets 里塞了不该塞的资源。我当时发现图标一套放了 5 种尺寸,有的甚至不是应用内用到的,删掉冗余资源后包体直接小了将近 2MB。
4.2 签名与 Profile
没有签名的 HAP 装不进真机,更上不了架。这里要稍微解释一下鸿蒙的签名体系,我当初第一次接触时也绕了很久。
鸿蒙应用签名需要两套凭据:证书和Profile。证书用来标识开发者身份,由华为 AGC 平台签发;Profile 描述这个应用具备哪些权限、支持哪些设备、使用哪个证书。用一个不严谨但好记的类比:证书是你的身份证,Profile 是盖章的通行证,App 安装包两样都得带齐。
签名方式有自动和手动两种。用 DevEco Studio 做自动签名最省事,登录华为账号后 IDE 会自动生成调试证书和 Profile。但命令行场景下手动签名的步骤我得完整列出来,因为上架的正式包必须走手动签名逻辑。
手动签名的核心命令是用hap-sign-tool.jar:
java -jar hap-sign-tool.jar sign-app \ -keyAlias "release_key" \ -signAlg "SHA256withECDSA" \ -mode "localSign" \ -appCertFile "release.cer" \ -profileFile "release.profile" \ -inFile "entry-default-unsigned.hap" \ -outFile "entry-release-signed.hap"这些文件的来源是:在 AGC 平台上创建应用后,配置并下载发布证书和发布 Profile。这里有个细节要特别提醒——Profile 和应用的 bundleName 必须一一对应。我说“必须”,是因为这个错了签名阶段大概率不报错,但一安装到真机上就会直接提示安装失败,错误码ERR_APPEXECFWK_INSTALL_FAILED,排查起来特别迷惑。
4.3 hdc 真机调试
构建产物有了,签名也做了,怎么装到手机上?用hdc。
鸿蒙手机的开发者模式默认是隐藏的,需要在设置里连点“版本号”若干次才能开启。开启后插入 USB,手机会弹出 USB 调试授权框。一切正常后,用hdc list targets应该能看到设备序列号。
安装命令:
hdc install entry-release-signed.hap想覆盖安装调试包:
hdc install -r entry-debug.hap想抓崩溃日志,先hdc shell hilog过滤出当前应用的日志,这一步对排查应用闪退特别有用:
hdc shell hilog | grep BabySchedule真机联调这个环节我最想强调的一点是:不要只在模拟器上测完就算完。模拟器表现正常,不代表真机表现正常。尤其是提醒、通知这些和系统能力强相关的功能,模拟器和真机的行为差异很大。我的习惯是:每次改完核心逻辑,至少在真机上完整跑一遍“添加日程-锁屏-等提醒-收到通知-点击通知进详情”的链路,确认没断才继续写下一个功能。
5. 上架:从本地 HAP 到应用市场
5.1 开发者账号与实名认证
上架前需要注册一个华为开发者账号,并进行实名认证。个人开发者用个人身份认证就行,流程很简单,身份证信息加人脸识别,几分钟就通过。企业开发者则需要营业执照等信息,流程会长一些,如果是公司项目要提前规划好时间。
这里要特别说一句,开发者账号一旦注册,后面所有证书、应用记录都会挂在这个账号下,而且证书有有效期,过期了需要重新生成。我当时没注意证书有效期,上线前重新签名折腾了半天,这个教训写在这里。
账号就绪后,登录 AppGallery Connect 平台,也就是 AGC,后续的证书申请、应用创建、版本管理都在这里操作。
5.2 创建应用与填写资料
在 AGC 后台点击“创建应用”,需要填的关键信息包括:应用名称、应用包名、应用分类、语言、图标等。
应用名称会展示在应用市场上,和安装到手机桌面上显示的app.json5里的label不完全是一回事,后者是桌面显示名。两者最好一致,避免用户混淆。
应用分类这里容易踩坑。我当时给“宝贝日程表”选的分类是“儿童”,没想到审核人员反馈说这个分类需要额外提供儿童隐私保护说明。后来我重新审视了一下产品,这个 App 主要使用对象虽然是孩子,但它是家长配置日程、孩子查看信息,严格说属于“日常生活”类工具,换成“生活”分类后审核就顺利通过了。
另外,图标要求必须是 512x512 像素,这个很多人都知道;但截图尺寸和数量很多人会忽略。AGC 要求至少上传 3 张应用截图,且分辨率需要覆盖主流手机屏幕比例。我刚开始只传了 2 张,直接被驳回要求补图。
5.3 上传与审核
资料填完,就可以上传已经签名好的 release HAP 包。上传的位置在 AGC 后台的“应用信息”->“版本管理”里,上传后填写版本更新说明,然后提交审核。
审核周期在不同时期波动很大,我等过最快的一天,也遇到过一次拖了三四天。期间 AGC 会发邮件通知审核状态,如果被驳回,邮件里会写清楚原因,后台也能看到审核意见。
提审之前,强烈建议自己在真机上把 HAP 装一遍,完整走一遍核心流程。很多驳回原因不是技术问题,而是“运行崩溃”“启动黑屏”“功能无法使用”这些基础问题。我一个朋友提审一个工具类 App,因为没在真机测过,结果首次启动申请敏感权限时崩溃,直接被拒,来回改了好几天。
5.4 审核被拒的常见原因
结合我自己的经历和圈内交流,审核被拒主要集中在以下几类:
- 应用分类不准确,尤其涉及儿童、健康、金融等敏感分类。
- 缺少隐私政策链接,凡是涉及用户信息收集的应用都必须提供。
- 权限申请与功能不匹配,比如一个日程工具不需要读取短信,申请了就会被问询。
- 应用截图与真实界面不符,或者是用模拟器截的图布局变形。
- 版本号问题,versionCode 必须比上一个版本大,否则无法提交。
这些都不是功能问题,纯粹是资料和合规细节,但正是这些琐碎细节决定了上架流程顺不顺利。我自己的体会是:提审前把 AGC 后台的每一项配置当成产品的一部分来对待,宁可在后台多看几遍,也别让自己在审核排队里耗时间。
6. 问题排查速查与实战心得
6.1 常见问题速查表
整个开发到上架的过程中,我记录了一批高频问题,整理成表格放在这里,方便以后直接搜:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
ohpm install超时或拉不到依赖 | 仓库源访问不稳定 | 检查 ohpm registry 配置,切换到可用镜像源 |
构建报SDK component missing | SDK 未安装完整 | 用 DevEco Studio 的 SDK Manager 补装或用命令行检查 SDK 组件 |
hdc list targets看不到设备 | 未开启开发者模式/驱动未安装 | 开启 USB 调试,检查连接线,重启 hdc server |
真机安装报ERR_APPEXECFWK_INSTALL_FAILED | 签名证书与 Profile 不匹配 | 确认 release 包的证书和 Profile 来源一致,重新签名 |
签名时报certificate not yet valid | 系统时间偏差 | 同步系统时间,再执行签名 |
| 提醒到点没触发 | 应用被强制停止过/没有权限 | 确认权限声明正确,重新打开 App 后再测试 |
表格里有些问题一眼看上去不是大问题,但真遇到时最花时间。比如签名时报时间有效期不对,我查了好半天才发现是测试机器时间慢了几天,导致系统认为证书还没生效。
6.2 几个让开发体验更好的小习惯
项目做完,我复盘了一下整个流程,整理出几个今后写鸿蒙 App 一定会保留的习惯。
第一,所有构建、签名、安装命令脚本化。我把这次用到的命令整理成了一个build.sh,里面做了严格的分步控制:先拉依赖,再构建,再签名,再安装。以后不管谁拿到这个工程,跑一遍脚本就能出包,省去口头沟通的麻烦。
第二,DataManager 层做统一封装。数据库、提醒、设置都通过统一入口调用,模块之间不直接互相操作。比如“删除日程”这个动作,在 Manager 层里会同时做三件事:删数据库记录、删系统提醒、返回删除结果。这样上层页面只管调用,不用关心底层联动逻辑,代码清晰很多,后期加云端同步也容易扩展。
第三,每轮迭代都重新做一遍全链路验证。哪怕这次改动只是改了一个按钮颜色,只要影响了页面结构,我就会重新打包、签名、安装,然后走一遍核心路径。很多线上的坑,其实在发布前只要多花十分钟做一遍主干流程,就能提前发现。
6.3 关于 DevEco CLI 的现状与展望
最后聊几句我对 DevEco CLI 这套东西的整体感受。它现在的形态更像是一个“工具合集”,还没有像 npm 对前端那样形成一套极致的标准工作流体验,但已经能把一个应用从源码到上架包的链路完整串起来。对个人开发者来说,它的价值不只是省时间,更在于让整个构建过程透明化、可重复化,这对于做技术复盘和问题定位特别重要。
如果你是从 IDE 转命令行,我建议不要急着全部切换。先在熟悉的 IDE 里写完功能,再尝试用命令行构建一次,看看产物目录里多了什么;然后再试着用命令行安装到真机;最后再挑战手动签名和上架。这样一步步过渡,既不会因为陌生而挫败,又能逐步掌握整条链路。
这次“宝贝日程表”从产品想法到正式上架,前后花了两周多的时间,真正写业务代码的时间其实就三四天,剩下的时间全消耗在构建、签名、资料审核这些“看不见”的环节上。但恰恰是这些环节,决定了你的应用能不能体面地出现在用户面前。希望这篇记录能让你少走一点我走过的弯路。