用 ArkTS + ArkUI 搭一个可上架的鸿蒙工程骨架

这是《鸿蒙开口练APP实战手记》的第一篇。这个系列不写"Hello World 复读",所有内容都来自一个真实项目:一款 AI 口语表达教练 App,HarmonyOS 平台,ArkTS + ArkUI,从空白工程一路做到上架。开篇先解决最基础、也最容易被糊弄过去的问题——工程骨架

很多人的第一个鸿蒙工程是 DevEco Studio 模板生成的,能跑就收工。等到要上架才发现:签名没配、版本号没规划、权限声明乱塞、构建只会点按钮。这篇文章把"可上架"倒推到第一天,逐项讲清楚。

1. 先定 SDK 基线:compile 24 / target 24 / compatible 23

鸿蒙工程的 SDK 有三个数字,对应build-profile.json5里的字段:

{ "products": [ { "name": "default", "signingConfig": "default", "targetSdkVersion": "6.1.1(24)", "compatibleSdkVersion": "6.1.0(23)", "runtimeOS": "HarmonyOS" } ] }
  • compileSdkVersion:用什么 SDK 编译,决定你能调用哪些 API。在 DevEco 里随 SDK 安装确定,选最新稳定版即可。

  • targetSdkVersion:声明"我为这个版本做过完整适配",系统按这个版本的行为对待你的应用。不要填一个你没真机验证过的版本。

  • compatibleSdkVersion:最低兼容版本。填 23 意味着 API 23 的设备也能装,代价是你用到 API 24 独有能力时必须自己做分支判断。

我们的选择是24/24/23,理由很朴素:target 跟上最新稳定版拿到完整行为,compatible 下探一级多覆盖一批存量设备,同时把"API 23 真机验证"写进验收清单,防止基线只是纸面数字。

这个决定应该在项目第一张 ADR(架构决策记录)里冻结,因为它影响后面每一个系统能力的选型——比如我们后面选 CoreSpeechKit 做离线语音识别,就是先确认了它在 API 23 上行为完整。

2. 工程全景:三个配置文件各管一件事

一个标准鸿蒙工程的根目录长这样:

SpeakLab/ ├── AppScope/ │ └── app.json5 # 应用级元数据(全局唯一一份) ├── entry/ # 主模块(entry 类型,装入口) │ └── src/main/ │ ├── module.json5 # 模块级声明(Ability、权限、页面) │ ├── ets/ # ArkTS 源码 │ └── resources/ # 资源(字符串、颜色、媒体、rawfile) ├── build-profile.json5 # 构建配置(签名、产物、构建模式) ├── oh-package.json5 # 依赖声明 └── hvigorfile.ts # 构建脚本入口

新手最容易混淆的是前三个文件的分工,一句话记法:

文件

管什么

类比

AppScope/app.json5

这个应用是谁:bundleName、vendor、版本号、图标、名称

Android 的 applicationId + versionCode

entry/src/main/module.json5

这个模块有什么:Ability、页面路由、权限声明

AndroidManifest.xml

build-profile.json5

怎么构建:签名、SDK 版本、buildMode

build.gradle

app.json5:上架信息的第一现场
{ "app": { "bundleName": "com.xiangshikeji.speaklab", "vendor": "xiangshikeji", "versionCode": 1, "versionName": "1.0.0", "icon": "$media:layered_image", "label": "$string:app_name" } }

三个纪律,都是从上架返工里学来的:

  1. bundleName 一次定终身。上架后不可改,用反域名且和主体一致,别用com.example.xxx起步。

  2. versionCode 是上架的单调轴。首发定 1,之后每次提交至少 +1;versionName 给人看,versionCode 给市场看。

  3. 名称和图标走资源引用$string:/$media:),不要硬编码。用户可见名称收敛在一个字符串资源里,后续改名字、做多语言都只动一处。

module.json5:权限和 Ability 的声明处
{ "module": { "name": "entry", "type": "entry", "mainElement": "EntryAbility", "deviceTypes": ["phone"], "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "entities": ["entity.system.home"], "actions": ["ohos.want.action.home"] } ] } ], "requestPermissions": [ { "name": "ohos.permission.MICROPHONE", "reason": "$string:sl_mic_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.INTERNET" } ] } }

两个要点:

  • user_grant 权限必须给 reason,且 reason 走资源ohos.permission.MICROPHONE这类用户授权权限,审核会看你声明的理由文本。reason引用字符串资源而不是写死,方便后续按审核意见调整文案。

  • 权限声明是"最小集"纪律的起点。工程第一天就要忍住"先都加上再说"的冲动——每多一个权限,上架审核就多一份解释成本。我们只有麦克风(业务必需)和 INTERNET(AI 功能必需)两个。

build-profile.json5:签名与严格模式
{ "app": { "signingConfigs": [ { "name": "default", "type": "HarmonyOS", "material": { "certpath": "/Users/xxx/.ohos/config/default_xxx.cer", "keyAlias": "debugKey", "profile": "/Users/xxx/.ohos/config/default_xxx.p7b", "signAlg": "SHA256withECDSA", "storeFile": "/Users/xxx/.ohos/config/default_xxx.p12" } } ], "products": [ { "name": "default", "signingConfig": "default", "buildOption": { "strictMode": { "caseSensitiveCheck": true, "useNormalizedOHMUrl": true } } } ] } }
  • 签名从第一天就配好。DevEco 的自动化签名会在~/.ohos/config/生成调试证书并写回这个文件。哪怕只是真机调试,鸿蒙也要求签名 HAP——先把这条链路跑通,免得"能编译不能装机"卡住节奏。注意:keyPassword/storePassword是 DevEco 托管的密文,这个文件不要提交到公开仓库。

  • strictMode 两个开关建议开caseSensitiveCheck强制 import 路径大小写敏感——macOS 文件系统默认不敏感,不开这个,代码在 CI 或同事机器上会以莫名其妙的方式挂掉;useNormalizedOHMUrl统一模块 URL 规范,避免依赖解析的隐性分叉。

3. 源码目录:第一天就分层

entry/src/main/ets/下的目录结构,决定了三个月后这个工程还能不能维护。我们的约定:

ets/ ├── entryability/ # EntryAbility(唯一入口 Ability) ├── pages/ # 页面 Destination(home / settings / report / history…) ├── sheets/ # 全局弹层(权限说明、统计、教练历史) ├── common/ │ ├── components/ # 可复用 UI 组件 │ ├── theme/ # 语义化主题 token │ ├── navigation/ # 路由与壳层 │ ├── types/ # 领域类型 │ ├── store/ # 状态管理 │ ├── lexicon/ # 领域服务:词库 │ ├── asr/ # 系统能力 port:语音识别 │ ├── ai/ # 系统能力 port:AI 调用 │ └── settings/ # 持久化 port:设置 └── spike/ # 技术验证代码(与正式代码物理隔离)

核心规则只有两条:

  1. 页面不直接碰系统 Kit。语音识别、AI 网络调用、权限申请,全部收口到common/下的 port 层,页面只依赖自己的 service 接口。这条规则让"换实现"和"写假数据"都变成只动一处的事。

  2. 命名前缀统一。类/服务统一SpeakLab前缀,资源统一sl_前缀(如$string:sl_mic_reason),日志 tag 统一SpeakLab。前缀看起来是小事,但当你要在 400 多条测试日志或整包字符串资源里grep 时,它就是救命绳。

4. 命令行构建:别只会点按钮

DevEco 的 Run 按钮很方便,但可复现的构建必须能在命令行完成——这是后续做 CI、做验收、做"干净重建"的前提。两个环境变量是关键:

export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk export PATH="/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin:\ /Applications/DevEco-Studio.app/Contents/tools/node/bin:$PATH"

注意DEVECO_SDK_HOME指向Contents/sdk这一层,不要指到里面的default/子目录——这是新手最常见的踩坑点,指错了 hvigor 会报找不到 SDK 组件。

然后一条命令打出签名包:

hvigorw --mode module \ -p product=default \ -p module=entry@default \ -p "buildMode=release" \ assembleHap --no-daemon

产物在entry/build/default/outputs/default/entry-default-signed.hap。工程里把它包一层脚本(scripts/build-entry-hap.sh),构建前先hvigorw clean、构建后打印 HAP 路径、大小和 SHA-256:

scripts/build-entry-hap.sh release # build-entry-hap: release HAP ready # build-entry-hap: path=.../entry-default-signed.hap # build-entry-hap: size=xxM sha256=f09f7a65...

为什么要打印 SHA-256?因为"验收构建"和"演示构建"必须是同一个东西。Hash 一贴,谁都没有歧义。这个小习惯在后面做独立验收时救过我们很多次。

装到真机:

hdc install -r entry/build/default/outputs/default/entry-default-signed.hap

5. 小结:骨架的检查清单

到这里,一个"朝着上架去"的鸿蒙工程骨架就齐了。按清单自查:

  • SDK 基线(compile/target/compatible)写进 ADR,最低版本有真机验证计划

  • app.json5:bundleName 定终身、versionCode 从 1 起、名称图标走资源引用

  • module.json5:权限最小集,user_grant 权限的 reason 走字符串资源

  • build-profile.json5:签名链路跑通,strictMode 两个开关打开

  • ets/分层:pages / sheets / common 各司其职,系统能力收口 port 层

  • 命名前缀统一(SpeakLab/sl_

  • 命令行能 clean 构建出签名 HAP,且打印 SHA-256