1. 立项背景与需求拆解
1.1 为什么做“宝贝日程表”而不是其它 App
做鸿蒙开发这行,最常被问的一句话就是“能不能用个小项目带我入门?”市面上的教程项目,要么是待办清单,要么是记事本,看完确实能学会 ArkTS 语法,但离“上架”这个目标太远。这次我选了一个非常贴近真实生活的场景——给家里孩子做一个专属日程表,主打“语音提醒 + 周视图 + 贴纸奖励”,用户画像很清晰:家里有 3 到 8 岁孩子的家长。
这个 App 的业务逻辑不复杂,但也不是 Hello World 级别:它需要处理周视图的日期计算、重复提醒的触发逻辑、数据本地持久化,还要做一张能让孩子觉得“好玩”的贴纸奖励墙。这些功能覆盖了 ArkTS 的 UI 状态管理、Worker 线程、@StorageLink 数据持久化、通知服务等多个常用 API,做完这一圈,基本就把日常开发 80% 的高频场景练到了。
更重要的是,这个项目全程只用到 DevEco CLI,不打开 DevEco Studio 图形界面。很多做自动化构建、CI/CD 接入的同学对命令行工具很感兴趣,但官方文档对 CLI 的实操案例讲得少,这次我把从新建工程到上架审核的完整链路都跑了一遍,有资格聊聊坑在哪。
1.2 功能边界与目标用户
先明确“宝贝日程表”的版本边界,避免后期扯皮:
| 功能模块 | v1.0 范围 | 不做的事 |
|---|---|---|
| 日程管理 | 按时间段添加任务,支持重复规则 | 不做农历、不做节假日判断 |
| 周视图 | 周一到周日切换,左右滑动 | 不做月视图、年视图 |
| 提醒通知 | 本地通知,按“提前5分钟/准时”触发 | 不做跨设备同步 |
| 贴纸奖励 | 完成任务得一枚贴纸,攒 5 枚解锁动画 | 不做社交分享、排名 |
| 数据存储 | RelationalStore(SQLite)存本地 | 不做云备份、账号体系 |
目标设备是鸿蒙手机和平板,最低支持 API 9。为什么不做账号体系?因为这个版本的核心目标是“让家长快速能用”,任何注册流程都会砍掉一半的转化率。数据存本地对隐私也是好事,家庭日程本来就不该上传到云端。
2. 环境准备与 DevEco CLI 第一印象
2.1 CLI 工具链的安装与版本约定
先把基础环境跑通。不建议用 DevEco Studio 自带的命令行工具,我测试下来,直接用 hvigor 独立包更稳定。步骤如下:
# 1. 确认 Node.js 版本,必须 16.19.1 以上 node -v # 2. 安装 Ohos 工具链(兼容 DevEco CLI) npm install -g @ohos/hvigor npm install -g @ohos/hvigor-ohos-plugin # 3. 安装 DevEco CLI 主程序(当前版本 5.0.x) npm install -g @deveco/cli我踩过的第一个坑就在这里:hvigor 的版本必须和 SDK 匹配。如果你安装的是 API 12 的 SDK,却用 hvigor 4.x 去构建,会直接报hvigor version too low。建议在项目根目录的hvigor/hvigor-config.json5里固定版本号:
{ "modelVersion": "5.0.0", "dependencies": { "@ohos/hvigor": "5.0.0", "@ohos/hvigor-ohos-plugin": "5.0.0" } }另外,Windows 用户建议把安装目录加到 PATH 时用C:\Users\你的用户名\AppData\Roaming\npm,别用管理员权限装全局包,否则后续项目会有权限错乱。
2.2 用 CLI 创建工程骨架
用 CLI 创建工程,命令比我想象的要简洁:
# 创建项目,工程名 baby-schedule deveco create --project baby-schedule --template empty --target device # 进入目录,安装依赖 cd baby-schedule npm install--template empty是纯 ArkTS 空工程,不会带一堆示例代码。--target device表示默认构建真机版本,不生成模拟器专用包。生成目录结构如下(只列关键部分):
baby-schedule/ ├── AppScope/ │ └── app.json5 ├── entry/ │ ├── build-profile.json5 │ ├── hvigorfile.ts │ └── src/ │ ├── main/ │ │ ├── ets/ │ │ │ ├── entryability/ │ │ │ └── pages/ │ │ ├── resources/ │ │ └── module.json5 │ └── ohosTest/ └── build-profile.json5这里有一个细节值得注意:deveco create默认生成的包名是com.example.baby_schedule,上架时包名不能改,所以创建前一定想好。我这次直接用的com.littlepapa.babyschedule,听起来就比 example 正经多了。
3. 核心代码开发与关键模块实现
3.1 架构设计:状态管理选型
日程表这个场景有两个特点:数据量不大,但更新频率高;UI 层需要同时感知“当前周”和“选中日期”的变化。我用的是三层架构:
- 数据层:RelationalStore 封装成 ScheduleRepository,负责增删改查
- 状态层:每个页面持有一个
@Observed的 ViewModel,通过@State绑定 UI - 逻辑层:日程提醒的计算放在独立工具类,便于单元测试
为什么不直接用@StorageLink全局存储?因为日程表的数据有复杂的查询逻辑(按月、按周、按天),而AppStorage适合的是轻量级 KV 数据。数据层用 SQLite 才是最合理的选择。
3.2 周视图的日期计算
周视图是“宝贝日程表”最核心的交互。用户向左滑是下一周,向右滑是上一周,焦点默认定位到今天。这里的难点是“如何把任意日期归位到它所在的周一”。
// 获取某天所在周的周一日期 function getMondayOfWeek(date: Date): Date { const day = date.getDay(); // 0 是周日 const diff = day === 0 ? 6 : day - 1; const monday = new Date(date); monday.setDate(date.getDate() - diff); monday.setHours(0, 0, 0, 0); return monday; } // 获取本周七天的日期列表 function getWeekDays(anchorDate: Date): Date[] { const monday = getMondayOfWeek(anchorDate); const days: Date[] = []; for (let i = 0; i < 7; i++) { const d = new Date(monday); d.setDate(monday.getDate() + i); days.push(d); } return days; }这里有个 JavaScript 的经典坑:getDay()返回 0 表示周日,而不是周一。很多新手到这里会算错。另外,日期计算要用setDate而不是setTime,因为要考虑夏令时和时区偏移,用毫秒加减容易踩坑。
UI 层面,周视图我用Swiper组件,每个页面显示一周。为了让 Swiper 可以无限滑动,我用了一个“假分页”技巧:初始 index 设为 500,通过getMondayOfWeek动态计算当前显示周。这样即使用户狂滑 100 周,也不会出现数组越界。
3.3 提醒功能实现:Worker + 本地通知
日程提醒是这个 App 的“灵魂功能”。家长设置“每天 18:00 提醒练琴”,到了时间就得响通知。我用了两个组件配合:
- Worker 线程:每 5 分钟检查一次最近的日程是否到达提醒时间(不阻塞 UI 线程)
- 本地通知:通过
@ohos.notification发送提醒
Worker 的代码逻辑如下:
// worker.ts import worker from '@ohos.worker'; const workerInstance = new worker.ThreadWorker('entry/ets/workers/ReminderWorker.ts'); // 主线程发送检查指令 workerInstance.postMessage({ type: 'CHECK_REMINDER' }); // 处理返回结果 workerInstance.onmessage = (event) => { const reminder = event.data; if (reminder && reminder.shouldNotify) { postNotification(reminder.title, reminder.message); } };这里有一个特别需要注意的坑:Worker 文件在 DevEco CLI 环境下,路径拼写一定要和module.json5里的worker配置对应。CLI 不会像 Studio 那样自动帮你补路径,拼错了直接报Worker init failed。
关于通知权限,API 9 开始要动态申请:
import notification from '@ohos.notification'; notification.requestEnableNotification() .then(() => { console.info('Notification enabled'); }) .catch((err) => { console.error(`Failed: ${err.code}`); });注意,申请权限的时机要放在用户“第一次设置提醒”的时候,而不是 App 启动时就弹窗。否则用户还没感知到产品价值,就被权限弹窗劝退了,转化率非常难看。
3.4 数据持久化:RelationalStore 建表与事务
日程数据我用一张schedule表,字段如下:
CREATE TABLE IF NOT EXISTS schedule ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, start_time INTEGER NOT NULL, -- 时间戳(毫秒) end_time INTEGER NOT NULL, repeat_rule TEXT, -- NONE / DAILY / WEEKLY / WEEKDAYS reward_sticker INTEGER DEFAULT 0, -- 获得贴纸数 completed INTEGER DEFAULT 0 );插入数据时有一个细节:用事务批量写入。比如家长一次性添加了 7 天课程表,如果你一条一条 insert,性能会肉眼可见地卡顿。正确做法是把所有 insert 放到一个事务里,实测在低端设备上,100 条数据的写入时间能从 800ms 降到 50ms 左右。
store.executeSql('BEGIN TRANSACTION', []); // 循环插入多条数据 for (let item of scheduleList) { await store.executeSql(INSERT_SQL, [item.title, item.startTime, ...]); } store.executeSql('COMMIT', []);查询方面,因为 UI 只需要某一天的数据,所以 SQL 里直接加时间范围条件,不要全表查出来再在 JS 层过滤。数据量少时无所谓,但养成好习惯,以后做复杂项目才不会翻车。
3.5 贴纸奖励墙:用 LazyForEach 优化渲染
贴纸墙是给孩子看的“正反馈”,也是这个产品最需要个性感的地方。每完成一个任务,就在日历对应日期上加一枚小贴纸。这里我用的是LazyForEach渲染贴纸图标,避免一次加载 100 张图片卡顿。
LazyForEach(this.stickerDataSource, (item: Sticker) => { ListItem() { Image(item.icon) .width(48) .height(48) } }, item => item.id.toString())注意第三参数keyGenerator,一定要返回唯一且稳定的字符串。如果直接返回 index.toString(),删掉中间一张贴纸时,后面的图标会错位复用,渲染会出诡异 bug。这是一个我在实测中踩出来的坑,网上很多教程都没提。
4. 构建打包与签名调试
4.1 Debug 包构建与自动化测试
开发调试时不需要每次都打签名包,直接用两条命令搞定:
# 执行单元测试 deveco test --project baby-schedule # 构建 debug 包 deveco build --mode debugCLI 构建完成后,HAP 文件输出在entry/build/default/outputs/default/entry-default-unsigned.hap。这个 unsigned 包不能直接安装到真机,但可以通过 hdc 安装签名后的包。
自动化测试这里我建议把核心算法(日期计算、重复规则判断)写成纯 TypeScript 的单元测试,不要依赖 UI 环境。比如“每周一、三、五重复”的规则,用数据驱动的方式测一遍:
// test/repeatRules.test.ts describe('RepeatRuleValidator', () => { test('should match weekday rule', () => { const rule = parseRepeatRule('WEEKDAYS'); expect(rule.matches(new Date('2024-01-15'))).toBe(true); // 周一 expect(rule.matches(new Date('2024-01-14'))).toBe(false); // 周日 }); });这类测试跑得快,能兜住回归风险,比手动点界面靠谱得多。
4.2 签名配置:证书、Profile 与命令行签名
上架前必须生成签名好的 HAP。签名流程分三步走:
第一步:生成密钥和 CSR 文件。
# 生成 RSA 密钥对 keytool -genkey -alias babyschedule -keyalg RSA -keysize 2048 \ -keystore babyschedule.p12 -storetype PKCS12 # 生成 CSR 上传至 AGC keytool -certreq -alias babyschedule -keystore babyschedule.p12 \ -file babyschedule.csr第二步:在 AGC 后台添加 App,上传 CSR 获取证书(.cer),然后配置 Profile 文件。
第三步:在build-profile.json5里指定签名信息:
{ "app": { "signingConfigs": [ { "name": "default", "type": "HarmonyOS", "material": { "certpath": "证书路径.p12", "storePassword": "你的密码", "keyAlias": "babyschedule", "keyPassword": "你的密码", "profile": "xxx.p7b" } } ] } }配置好之后,构建时加一个参数就会自动签名:
deveco build --mode release --sign提示:密码不要硬编码在
build-profile.json5里提交到 Git,建议用环境变量注入。我一般会在 CI 里这样设置:STORE_PASSWORD=${STORE_PASSWORD},配合华为云CodeArts或GitHub Actions的 Secrets 管理。
4.3 真机调试:把 HAP 装到手机上
命令行装包的姿势如下:
# 连接设备 hdc list targets # 安装签名后的 HAP hdc install entry/build/default/outputs/default/entry-default-signed.hap # 启动应用 hdc shell aa start -b com.littlepapa.babyschedule -a MainAbility调试日志用hdc hilog抓取,比在 Studio 里看 logcat 更灵活:
# 实时输出包含关键字 Reminder 的日志 hdc shell hilog | grep Reminder5. 从“能跑”到“能上架”的完整流程
5.1 AGC 上架前的合规检查
很多开发者有这种经历:本地能跑,一上架就被打回。这次在 AGC 上架“宝贝日程表”也收到了两个整改意见,我整理一下供参考:
隐私政策缺失:只要 App 涉及任何数据收集(哪怕是本地存储),AGC 审核都要求提供隐私政策。我的解决方式是写了一个简单的隐私声明页,并在 App 首次启动时弹窗展示,同时在 AGC 后台录入隐私政策网址。
权限申请说明不明确:之前代码里声明了ohos.permission.INTERNET,但实际上用不到,被审核团队质疑。建议上架前用一个脚本审计所有权限,只保留必要的:
grep -rn "permission" entry/src/main/module.json5删掉多余权限申请,不仅过审更快,也能让用户更放心。
5.2 应用信息完善与图标尺寸
上架需要上传的素材都是有明确规格的,一次性备好能省很多时间:
| 素材 | 规格要求 | 备注 |
|---|---|---|
| 应用图标 | 216×216 PNG,透明或非透明均可 | 不能有圆角,系统会自动裁切 |
| 宣传图 | 建议 800×800,展示核心功能 | 突出“一周视图”和“贴纸墙” |
| 五张应用截图 | 建议使用真机截屏,分辨率匹配目标机型 | 不要加边框 |
推荐用真机 Pixel 级截图,不要用模拟器截图,清晰度和显示效果差一个量级。另外,截图里的内容包括日期、数据都要真实自然,别用“11月31日”这种低级错误。
5.3 审核周期与版本迭代节奏
我提交审核的时间是周四上午,过审大概花了 2 个工作日。有一点比较坑:如果 App 有内购,审核前必须先配置好商品信息,否则会因为“虚拟支付功能未配置”被拦截。
上架之后,版本更新走的是 AGC 后台的“发布新版”流程。CLI 构建出新的 HAP 后,在后台直接上传替换即可。这里我强烈建议在versionName上加构建时间,方便排查用户反馈的版本问题:
{ "versionCode": 10003, "versionName": "1.0.3.20240108", }5.4 发布后的数据埋点与用户反馈
正式上架不是项目的终点。我提前接入了鸿蒙的分析 SDK,只统计几个核心事件:schedule_created、reminder_triggered、sticker_unlocked。埋点不需要很复杂,但要能回答一个问题:“用户是否完成了产品的核心闭环”。
实测第一批用户的数据来看,最受欢迎的是贴纸奖励墙——很多家长截图分享到家庭群。反而是提醒功能的使用率低于预期,我猜测是权限弹窗的时机太早,很多用户在设置日程前就拒绝了通知权限。这个发现直接影响了下一版的交互设计。
6. 常见问题与排查技巧实录
6.1 构建失败类问题速查表
我整理了自己开发过程中遇到的高频问题,按场景分类,方便大家快速对照:
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
hvigor command not found | hvigor 没安装或版本不匹配 | 检查hvigor-config.json5的 modelVersion,npm install -g @ohos/hvigor |
ohpm install 超时 | 网络问题或镜像源慢 | 配置镜像源:ohpm config set registry https://repo.harmonyos.com/ohpm/ |
SigningConfig is missing | 没有配置签名材料 | 检查build-profile.json5里的 certpath 和 profile 路径 |
Worker init failed | Worker 路径配置错误 | 检查module.json5的extensionAbilities中 worker 路径 |
安装 HAP 提示code: 9568320 | 非调试机安装签名包 | 需要在设备管理后台信任开发者证书 |
Native module not found | 第三方原生库未配置 | 检查oh-package.json5依赖和.so文件路径 |
6.2 ArkTS 语法层面的隐性坑
现在 ArkTS 已经支持绝大多数 TypeScript 语法,但有几点仍然要注意:
1. 不能用any类型。如果从 JSON 接口返回的数据没定义类型,直接用Record<string, Object>代替。CLI 构建时不会报错,但运行时会有类型转换的隐藏开销。
2. 状态管理的引用类型要谨慎。@State修饰数组或对象时,直接this.arr.push()不会触发 UI 更新。正确做法是重新赋值:
// 错误写法:this.scheduleList.push(newItem); // 正确写法: this.scheduleList = [...this.scheduleList, newItem];3. 页面路由前关掉定时器。如果页面有setInterval,在aboutToDisappear里一定要清掉,否则切后台再切回来,会出现多个 Worker 实例同时跑的诡异 bug。
6.3 上架审核被拒的补救思路
审核被拒不要慌,先看清楚 AGC 后台发的邮件,大概率是三类问题:
- 隐私政策不完整:补充“数据如何收集、如何使用、如何删除”三个环节的说明
- 权限声明与实际不符:逐个权限自检,把多余的权限申请删掉
- 截图描述与实际功能不符:有开发者用“美化效果”的截图来展示一个不含该功能的版本,这属于最容易被拒的情况
我这次就在隐私政策的“如何删除数据”部分补充了“在设置中清除缓存即可删除全部本地数据”的说明,第二天重新提交就过了。
7. CLI 自动化流程的进一步优化空间
7.1 把 CLI 接入 CI/CD 流程
整个项目开发到上架最值回票价的部分,是最后我把 CLI 构建流程接进了自己的 Webhook 脚本:每次git push到 master 分支,服务器自动执行构建、签名、跑测试,然后把 HAP 上传到指定目录。这样每次改完代码,不用开 IDE,直接给测试人员一个下载链接。
核心脚本这样写:
#!/bin/bash set -e echo ">>> Pulling latest code..." git pull origin master echo ">>> Installing dependencies..." npm install --registry=https://repo.harmonyos.com/ohpm/ echo ">>> Building HAP..." deveco build --mode release --sign echo ">>> Running tests..." deveco test --project baby-schedule echo ">>> Copying output..." cp entry/build/default/outputs/default/entry-default-signed.hap dist/babyschedule-latest.hap echo "Build complete."目前团队的日常开发,已经不需要人人装 DevEco Studio 了,命令行加一个趁手的代码编辑器就能完成绝大多数需求开发。这也印证了 DevEco CLI 的价值:它不只是 IDE 的附属品,而是独立、可图形化替代的完整开发链。
7.2 多产品矩阵的版本管理启发
做“宝贝日程表”期间,我也顺带用同样的一套 CLI 流程跑了另一个小项目(家庭记账),基本是把模板复制过来改改。建立一个自己的 CLI 模板仓库,把登录、隐私政策页、基础样式全部抽好,以后每次新项目都能省下至少 2 天重复工作。这算是我这次实战最深的心得:开发者要像搭积木一样开发 App,第一步就是准备一套趁手的积木。
根据这几周的实操体会,DevEco CLI 的整体成熟度已经能满足生产级应用开发,尤其在自动化构建和快速迭代这两个场景,体验非常顺滑。如果你正在学鸿蒙开发,或者正在苦恼项目怎么快速上架,真心建议直接用 CLI 走一遍全流程,很多原来 IDE 里“点点点”的黑盒操作被命令行揭开了面纱,理解会更透。希望这篇实战记录能让你少踩几个坑,把手上的鸿蒙项目顺利推向市场。