news 2026/9/16 3:42:38

鸿蒙开发实战:用DevEco CLI从零构建宝贝日程表到上架全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙开发实战:用DevEco CLI从零构建宝贝日程表到上架全流程

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 提醒练琴”,到了时间就得响通知。我用了两个组件配合:

  1. Worker 线程:每 5 分钟检查一次最近的日程是否到达提醒时间(不阻塞 UI 线程)
  2. 本地通知:通过@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 debug

CLI 构建完成后,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 Reminder

5. 从“能跑”到“能上架”的完整流程

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_createdreminder_triggeredsticker_unlocked。埋点不需要很复杂,但要能回答一个问题:“用户是否完成了产品的核心闭环”。

实测第一批用户的数据来看,最受欢迎的是贴纸奖励墙——很多家长截图分享到家庭群。反而是提醒功能的使用率低于预期,我猜测是权限弹窗的时机太早,很多用户在设置日程前就拒绝了通知权限。这个发现直接影响了下一版的交互设计。

6. 常见问题与排查技巧实录

6.1 构建失败类问题速查表

我整理了自己开发过程中遇到的高频问题,按场景分类,方便大家快速对照:

错误现象根本原因解决办法
hvigor command not foundhvigor 没安装或版本不匹配检查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 failedWorker 路径配置错误检查module.json5extensionAbilities中 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 里“点点点”的黑盒操作被命令行揭开了面纱,理解会更透。希望这篇实战记录能让你少踩几个坑,把手上的鸿蒙项目顺利推向市场。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:40:17

力扣101:对称二叉树的递归与迭代解法详解

不用引入太多背景&#xff0c;直接说结论&#xff1a;力扣第101题“对称二叉树”是一道非常典型的二叉树递归/迭代练习题&#xff0c;也是面试里出现频率很高的基础题。很多人在刚接触二叉树时&#xff0c;被遍历、深度、翻转这些概念绕晕&#xff0c;等做到“对称”这道题时又…

作者头像 李华
网站建设 2026/9/16 3:37:46

UVa 11509 Touring Robot:圆缩点与BFS网格搜索的几何避障解法

在做算法竞赛题目的时候&#xff0c;我最大的感受是&#xff1a;很多所谓“难”的题&#xff0c;其实不是代码量大&#xff0c;也不是某个算法特别复杂&#xff0c;而是题目本身披了一层“故事外衣”&#xff0c;你得把那层外衣剥掉之后&#xff0c;才能看到里面真正要求的东西…

作者头像 李华
网站建设 2026/9/16 3:37:39

基于DRO与CVaR的电力市场发电商自调度优化与MATLAB实现

电力市场里做日前自调度&#xff0c;最难的不是机组组合那套整数变量&#xff0c;而是电价到底怎么建模。你拿着历史场景做随机规划&#xff0c;第二天来个尖峰价格&#xff0c;利润直接被打回原形&#xff1b;改用区间鲁棒优化&#xff0c;又把最乐观的情况全丢掉&#xff0c;…

作者头像 李华
网站建设 2026/9/16 3:37:26

ASIL D认证RTOS与Microkernel:车规级功能安全的底层基石

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:37:10

星核精退场?从星核到开拓命途:星穹铁道4.5版本剧情猜想

昨晚临睡前刷社区&#xff0c;手指一顿&#xff0c;一条帖子标题把我钉在原地——《世间再无星核精&#xff01;开拓者接下来莫非要横扫六合&#xff1f;》【星穹铁道开拓者&#xff0f;星穹&#xff0f;4.5版本剧情】。说真的&#xff0c;看到"星核精"三个字我大腿都…

作者头像 李华
网站建设 2026/9/16 3:36:04

轻量级Markdown编辑器Markpad:打开即写,告别笨重全家桶

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华