鸿蒙记事本开发:工程结构、ArkTS持久化与签名安装

鸿蒙记事本开发:工程结构、ArkTS持久化与签名安装 简介基于鸿蒙系统开发的记事本项目实例面向希望了解HarmonyOS应用开发流程的移动开发者与学生。压缩包共127个文件包含37个Java源文件、46个XML布局与配置、26个PNG界面切图以及gradle构建脚本与JSON配置等体积仅11.63MB结构清晰便于直接导入DevEco Studio学习。已有1065人学习下载。资源围绕一个完整的记事本应用展开涉及鸿蒙微内核架构、分布式软总线与跨设备协同等核心特性通过Java与JS融合开发方式可学习界面UI设计、笔记数据的分布式存储与多设备同步以及应用测试、打包与分发的基本路径。包内附有完整工程目录适合想要快速上手鸿蒙开发、理解一次编写多端运行的读者参考。1. 一个“基于鸿蒙系统开发的记事本.zip”装的是什么场景你从代码仓库、网盘或者同事的移交清单里拿到“基于鸿蒙系统开发的记事本.zip”解压之后三分之二的概率得到一个完整的 DevEco Studio 工程剩下三分之一是编译好的 hap 包加安装说明。记事本在鸿蒙生态里的定位很特殊它同时覆盖 UI 渲染、数据持久化、系统弹窗、资源适配和应用生命周期五条主线复杂度卡在“能演示”和“能讲清楚”之间是鸿蒙开发者绕不开的样板工程。下面把 zip 里的东西逐层拆开工程目录、ArkTS 数据层、构建签名链路以及拿到包之后怎么验证、怎么排错。适合刚开始接触鸿蒙开发、需要拿记事本项目做技术验证的工程师也适合准备把安卓记事本向鸿蒙迁移的人先看一遍边界。2. 拆开 zip鸿蒙工程目录结构与每个文件的职责一个标准鸿蒙应用工程解压后不是“一个文件夹堆源码”而是 AppScope、entry 和工程级配置文件组成的多层结构。先看目录树基于鸿蒙系统开发的记事本/ ├── AppScope/ │ ├── app.json5 # 应用级配置包名、版本号、图标 │ └── resources/ # 应用级公共资源 ├── entry/ │ ├── build-profile.json5 # 模块级构建配置含签名材料引用 │ ├── hvigorfile.ts # 模块级构建脚本 │ ├── oh-package.json5 # 模块依赖声明 │ └── src/main/ │ ├── module.json5 # 模块清单ability、权限、入口 │ ├── ets/ │ │ ├── entryability/ # UIAbility 生命周期入口 │ │ └── pages/ # 页面列表页、编辑页 │ └── resources/ # 模块资源字符串、颜色、图标 ├── build-profile.json5 # 工程级构建配置 ├── hvigorfile.ts # 工程级构建脚本 └── oh-package.json5 # 工程级依赖声明拿到 zip 后先看根目录有没有hvigorw脚本有它说明这是一个可命令行构建的完整工程而不是某个编辑器导出的半成品。2.1 AppScope 与 entry两层骨架各管什么AppScope 管“整个应用”entry 管“一个可安装模块”。两者配置的差异决定了安装后系统的行为配置项AppScope/app.json5entry/src/main/module.json5bundleName应用唯一标识安装后不可改不重复声明versionCode整包版本号升级时单调递增不涉及abilities不涉及声明 UIAbility 与页面入口requestPermissions不涉及声明权限如 ohos.permission.READ_IMAGEVIDEO关键字段是 app.json5 里的bundleName它在设备上对应安装目录和 hdc 命令的包名参数。如果 zip 里同时出现多个版本的工程先比对 bundleName避免装完才发现“同名但内容不对”。2.2 resources 目录的限定符机制与字符串引用记事本工程的 resources 下至少有两个子目录base/element/string.json存放引用型资源base/media/放启动图标。鸿蒙的资源限定符机制如en_US、zh_CN、dark允许同一个 string.json 按设备语言和深浅色动态切换。因此代码里不要写字面量统一走资源引用{ string: [ { name: app_name, value: 记事本 }, { name: empty_hint, value: 还没有笔记点右下角新建 } ] }页面侧通过$r(app.string.empty_hint)引用。这样后续做多语言和深色模式时不需要改一行 ArkTS 逻辑只要在 resources 下按限定符建目录、放同名文件即可。2.3 module.json5 里决定“能否被桌面启动”的三个字段module.json5 是鸿蒙应用能否出现在桌面的关键。新手最常见的问题是资源、代码都对装完之后桌面没有图标。重点检查 abilities 数组{ module: { name: entry, type: entry, srcEntry: ./ets/entryability/EntryAbility.ets, abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, skills: [ { entities: [entity.system.home], actions: [action.system.home] } ], startWindowIcon: $media:startIcon, startWindowBackground: $color:start_window_background } ] } }三个字段缺一不可type必须是entryskills里必须同时出现entity.system.home和action.system.homesrcEntry指向真实的 UIAbility 文件。有人把type改成feature想按需加载结果桌面图标直接消失。在鸿蒙系统里这不是 bug是入口类型的语义约束。3. 用 ArkTS 写记事本关系型存储、列表渲染与编辑回写3.1 持久化选型relationalStore 比 preferences 更贴近真实记事本ArkTS 侧的数据持久化有两条常见路径preferences键值对和 relationalStoreSQLite 封装。如果只存“一条文本”preferences 够用但一个正常记事本要存多条、按时间排序、以后可能加标签和搜索relationalStore 是正确选择。它的 API 风格比裸 SQL 友好又保留了关系型查询能力维度preferencesrelationalStore数据模型key-value表 行 索引查询只能按 keyRdbPredicates 链式过滤排序不支持orderByAsc / orderByDesc适合场景配置项、标记位业务数据载体3.2 表结构、插入与查询的封装创建数据库和表放到独立模块里页面只面向这个模块的导出函数。常见做法是维护一个db/NoteStore.etsimport { relationalStore } from kit.ArkData; import { common } from kit.AbilityKit; const STORE_CONFIG: relationalStore.StoreConfig { name: notes.db, securityLevel: relationalStore.SecurityLevel.S1 }; const SQL_CREATE_NOTES CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, updated_at INTEGER NOT NULL ); export async function initStore(context: common.UIAbilityContext): PromiserelationalStore.RdbStore { const store await relationalStore.getRdbStore(context, STORE_CONFIG); await store.executeSql(SQL_CREATE_NOTES); return store; } export async function addNote(store: relationalStore.RdbStore, title: string, content: string): Promisenumber { const row { title, content, updated_at: Date.now() }; return await store.insert(notes, row); } export async function queryAllNotes(store: relationalStore.RdbStore) { const predicates new relationalStore.RdbPredicates(notes); predicates.orderByDesc(updated_at); const resultSet await store.query(predicates); const notes: NoteItem[] []; while (resultSet.goToNextRow()) { notes.push({ id: resultSet.getLong(resultSet.getColumnIndex(id)), title: resultSet.getString(resultSet.getColumnIndex(title)), content: resultSet.getString(resultSet.getColumnIndex(content)), updatedAt: resultSet.getLong(resultSet.getColumnIndex(updated_at)) }); } resultSet.close(); return notes; }securityLevel是必填字段S1 表示设备级数据不上云、不跨端迁移以后要接分布式能力再提到 S2 或 S3 并同步申请权限。用 resultSet 遍历时一定在 finally 里调用close()否则下一次查询可能因为连接未释放报ResultSet is closed。这是鸿蒙 ArkTS 与普通 SQL 驱动最容易忽略的资源管理差异。3.3 列表页 ForEach 与编辑页 update 回写列表页用 ArkUI 声明式语法。页面挂载后先拿数据库再加载数据import { relationalStore } from kit.ArkData; import { common } from kit.AbilityKit; Entry Component struct NoteListPage { State notes: NoteItem[] []; private store: relationalStore.RdbStore | null null; async aboutToAppear(): Promisevoid { this.store await initStore(getContext(this) as common.UIAbilityContext); this.notes await queryAllNotes(this.store); } build() { List({ space: 12 }) { ForEach(this.notes, (item: NoteItem) { ListItem() { Column() { Text(item.title).fontSize(18).fontWeight(FontWeight.Medium) Text(item.content).fontSize(14).fontColor(#888) } .width(100%) .padding(16) } }, (item: NoteItem) item.id.toString()) } .width(100%) .height(100%) } }ForEach 的第三个参数是键生成器必须返回稳定且唯一的字符串。用item.id.toString()而不是 content因为内容会变键一旦变了会导致整个列表重渲染表现为“列表莫名闪烁”。编辑页保存时走 updateexport async function updateNote(store: relationalStore.RdbStore, id: number, title: string, content: string): Promisenumber { const predicates new relationalStore.RdbPredicates(notes); predicates.equalTo(id, id); const row { title, content, updated_at: Date.now() }; return await store.update(row, predicates); }删除同理把 update 换成 delete参数只用 predicates。到这里记事本的核心数据闭环已经打通列表查、编辑改、保存写、下拉刷新重新查。4. 构建、签名与 zip 打包从源码到可分发产物4.1 hvigor 构建命令与产物目录拿到源码 zip 后第一件事不是解压就导入 IDE而是确认能否命令行构建。鸿蒙工程默认用 hvigorDevEco Studio 只是外壳命令行才是可复现的构建入口# 工程根目录先给构建脚本执行权限 chmod x hvigorw # 执行构建产物为 hap 包 ./hvigorw assembleHap --mode module -p productdefault -p moduleentryWindows 环境用hvigorw.bat。构建成功后产物位于产物路径作用判断点entry/build/default/outputs/default/entry-default-unsigned.hap未签名包文件名带 unsigned不能直接安装entry/build/default/outputs/default/entry-default-signed.hap可安装包看构建日志是否出现 Signing done注意assembleHap只能保证编译通过签名是否生效取决于 build-profile.json5 里 product 是否绑定了 signingConfig。没绑定就产出 unsigned 包此时即使文件名写着 signed也可能是上次构建的缓存以日志里的Signing ... done为准。4.2 签名配置自动签名与 build-profile.json5 手工绑定签名材料涉及三个文件.p12证书库、.cer应用证书、.p7b描述文件。最省事的方式是 DevEco Studio 里 File Project Structure Signing Configs 勾选自动签名由华为账号体系生成鉴权材料。命令行或持续集成场景则明确写入 entry/build-profile.json5{ name: default, signingConfigs: [ { name: notepad, type: HarmonyOS, material: { certpath: ./sign/notepad.cer, storePassword: ******, keyAlias: notepad, keyPassword: ******, profile: ./sign/notepad.p7b, signAlg: SHA256withECDSA, storeFile: ./sign/notepad.p12 } } ], products: [ { name: default, signingConfig: notepad } ] }keyAlias和storePassword必须与 p12 生成时完全一致。签名报错里出现keystore password was incorrect九成是密码里的$、%之类的字符被 json5 解析吞掉改成双引号包裹的原始字符串即可。4.3 两种 zip 交付形态与打包排除项zip 交付有两种形态对应不同接收对象。发给开发同事压缩整个工程根目录但必须排除构建产物zip -r NOTEPAD_SOURCE.zip . \ -x */entry/build/* \ -x */node_modules/* \ -x */oh_modules/* \ -x */.git/*不排除entry/build的实际问题很隐蔽别人解压后首次构建hvigor 会复用旧缓存里的绝对路径报outFile path is not found。发给测试人员则是压缩 hap 加安装说明zip NOTEPAD_RELEASE.zip \ entry/build/default/outputs/default/entry-default-signed.hap \ README_INSTALL.mdREADME 里写清楚hdc install entry-default-signed.hap和包名com.example.notepad。把源码和签名材料同时丢进同一个安装包 zip等同于把私钥公开到分发链路这是交付规范里最不该踩的一条。5. 拿到 zip 后的验证与安装四步确认 hap 能跑5.1 完整性校验与内容分类不急着解压先做完整性校验unzip -t 基于鸿蒙系统开发的记事本.zip输出No errors detected说明压缩包完整。接着判断内容类型unzip -l 基于鸿蒙系统开发的记事本.zip | head -20 file entry-default-signed.hap看列表里是entry/src/main/ets路径还是.hap文件。.hap本身也是 zip 格式file会输出Zip archive data这是正常现象不要误判。5.2 包名、签名、hdc 安装逐项排查核对包名优先于安装因为签名校验报错和包名错误容易互相混淆unzip -p entry-default-signed.hap module.json | grep -o bundleName:[^]*然后直接 hdc 安装报错信息就是最好的检查表hdc list targets hdc install entry-default-signed.hap报错关键字实际原因处理方式code signature check failedhap 未签名或证书过期重新签名后重装install sign info inconsistent包名与签名证书不匹配核对 bundleName 与证书绑定的包名error: install failed due to: uninstall设备上已有旧版本先hdc uninstall com.example.notepadno such file设备未连接或路径写错先hdc list targets确认设备在线安装成功后执行启动命令验证入口hdc shell aa start -a EntryAbility -b com.example.notepad能正常拉起页面说明入口、签名、资源三个关键点都通过了。5.3 用 hilog 过滤 bundleName 定位启动闪退如果启动即闪退先抓日志再猜原因。hilog 按包名关键词过滤hdc shell hilog -x com.example.notepad日志里出现Failed to get relation时优先检查 aboutToAppear 里是否在 UIAbilityContext 未就绪时调用了 getRdbStore出现白屏但不退出多半是资源限定符缺失对照 base 资源目录逐个补$r引用。对于 zip 内同时带.crc校验文件或签名目录的交付物优先怀疑是半打包产物直接向交付方要带 hvigor 构建日志的完整工程比在本地反复解压更能定位问题。本文还有配套的精品资源点击获取