从空白模板到真机运行:鸿蒙文件管家 AITest 一天开发实录
从空白模板到真机运行:鸿蒙文件管家 AITest 一天开发实录
本文记录一个 HarmonyOS 应用从需求调研、SDK 大版本迁移、编码、编译排错到真机安装运行的完整一天,以及途中踩过的十几个真实的坑。没有粉饰,没有"凭代码反推",全是聊天记录里真实发生过的事。
注:文中的服务器地址、API 路径、Key、设备标识等均已脱敏。
项目是什么
两件事,两个项目:
| 项目 | 角色 | 技术栈 |
|---|---|---|
| AITest | 鸿蒙客户端(本文主角) | ArkTS + HarmonyOS 6.1.0(23) |
| FileUploadServer | 远程文件服务器(已部署) | ASP.NET Core 10 + PostgreSQL |
服务器跑在公网(域名略),提供一套基于 API Key 的文件操作接口。客户端的目标很朴素:用户输入一个 Key 登录,然后能看文件列表、上传、下载、删除。不造后端,只对接 HTTP API。
服务器的认证方式简洁到可爱:受保护的文件接口,鉴权就靠 URL query 参数 ?key=<你的密钥>。密钥分两种--Admin(管全部文件,365 天有效,只能服务器本机创建)和 Temporary(只管自己的,24h 过期,公网可申请但依赖 IP 白名单)。
因为手机出口 IP 不固定、临时密钥又卡 IP 白名单,客户端不做密钥申请,让用户手动输入一个已有的 Admin Key 登录。这个决策从一开始就定了,省掉后面一大堆麻烦。
一、起点:调研、验证、决策
第一件事不是写代码,是先把服务器摸清楚。我用 curl 把几个关键端点都打了一遍:
| 测试 | 请求 | 结果 | 结论 |
|---|---|---|---|
| HTTPS 可达性 | 请求文件接口(不带 Key) | 401,0.13s | 在线,HTTPS 正常 |
| 无效 Key | 带一个乱造的 Key 请求 | Unauthorized |
鉴权符合预期 |
| 公开端点 | 请求公开文件接口 | 200 + JSON | 匿名可用 |
| Admin Key | 带新 Admin Key 请求文件接口 | 200 + 完整列表 | 可用 |
服务器确认可用后,SSH 到服务器本机调密钥管理接口创建了一个 Admin Key(有效期到 2027-08-11),作为开发期的长效密钥。创建完顺手拿真实响应扒了一下 FileItem 的完整字段--比服务端文档详细,含一堆客户端用不上的内部字段(storedFileName、encryptionVersion、storageMode 等),客户端只保留 id/fileName/fileSize/contentType/uploadedAt/isPublic/publicPath 七个。
接着用一轮结构化提问跟自己确认了几个关键点:Key 来源(SSH 建 Admin)、上传来源(相册图片 + 任意文件)、下载位置(公共下载目录)、是否做密钥申请入口(不做)。
二、意料之外的迁移:API 9 -> API 23
这是整个项目最没有预料到的一段。
工程最初是 API 9 的老模板,build-profile.json5 里 compileSdkVersion: 9,代码用 @ohos.* 命名空间。我本来建议"先用 API 9 开发,代码用兼容命名空间,后续再升"。用户很干脆:"我先装最新 api"。
装完一看,不是 API 11,也不是 12,是 API 23(OpenHarmony 6.1.0.32,DevEco Studio 6.1.1.300)。一个 API 9 到 API 23 的 14 个大版本跨度。
这跨度意味着配置文件格式几乎全变:
| 项 | API 9 旧格式 | API 23 新格式 |
|---|---|---|
| 构建工具版本 | hvigorVersion: 2.4.2 |
modelVersion: "5.0.0" |
| SDK 版本字段 | compileSdkVersion: 9 |
compatibleSdkVersion / targetSdkVersion: "6.1.0(23)"(字符串) |
| wrapper 脚本 | hvigor-wrapper.js |
hvigorw.js |
| 命名空间 | @ohos.* |
@kit.* |
手动改这种配置几乎一定踩坑。于是我没硬写,而是去翻 DevEco Studio 6.1 安装目录里自带的项目模板(plugins/codegenie-plugin/previewProjectTemplate/),把模板的 build-profile.json5、hvigor-config.json5 等标准配置拿来对照着改,同时确认了 hvigor 版本是 6.24.4。照模板改,一遍过。
这一段还有两个小坑:
- emoji 让 Edit 工具失配:改
doc/00-integration-plan.md末尾时,Edit 报String to replace not found,试了转义和原字符都不行--文档里有 emoji,old_string 和文件实际内容有不可见差异。最后改用 Write 整体重写。 - Write 覆盖前必须先 Read:某次直接 Write 一个已存在的
oh-package.json5报File has not been read yet。因为我之前是用cat看的,没用 Read 工具。补一次 Read 再 Write 就过了。
三、一口气写完代码
环境就绪后,按三层架构把代码一次性铺开:
- ApiConfig:集中管理服务器地址和 5 个端点 URL,
?key=统一encodeURIComponent。 - ApiService:基于
net.http的 JSON 请求(列表、删除、Key 校验),统一错误码映射,业务错误用自定义ApiException携带状态码。 - AuthService:
preferences做 Key 的本地持久化,存/取/清。 - FileService:封装
request.uploadFile/downloadFile,处理沙箱拷贝和进度回调。 - Index.ets:登录页,输入 Key -> 校验 -> 存 -> 跳转,支持自动登录。
- FileListPage.ets:列表页,上传/下载/删除/退出。
代码写完,会话结束。但没有验证编译--WSL 里 SDK 路径是 Windows 格式,DevEco 工具链也在 Windows 侧,跑不了 hvigor。我留了三个待验证风险点交给用户在 DevEco 里 sync:
compatibleSdkVersion: "6.1.0(23)"是从 SDK 的oh-uni-package.json推断的,没经 sync 验证。getContext(this)在字段初始化时可能报错。- 公共下载目录受 Scoped Storage 限制,当前下载到沙箱,后续优化。
这三个伏笔,后面应验了。
四、编译器教我做人:21 个 ERROR
用户在 DevEco 里一 Rebuild,炸了:21 个 ERROR、40 个 WARN,集中在三个文件。
这次最耗时的不是改代码,是逐个去 SDK 的 .d.ts 声明文件里查证每个 API 的真实形态。鸿蒙 API 在不同版本差异大、Kit 重组频繁,靠记忆写一定会错。事实证明的确如此。
坑 1:@kit.RequestKit 在 API 23 里根本不存在
FileService.ets 里 import { request } from '@kit.RequestKit' 报错,find "$SDK" -iname "*RequestKit*" 无任何结果。
连锁后果很严重:request 变成隐式 any,导致后面 UploadConfig/File/RequestData 等对象字面量全部报 arkts-no-untyped-obj-literals--8 条错误都是这一个 import 的连锁反应。
根因:@ohos.request.d.ts 文件头标注的是 @kit BasicServicesKit,request 实际由 @kit.BasicServicesKit 导出。改成 import { request } from '@kit.BasicServicesKit',8 条连锁错误瞬间消失。
坑 2:@kit.CoreFileKit 不导出 fs
import { fs } from '@kit.CoreFileKit' 失败。读声明文件发现 CoreFileKit re-export 的是 fileIo。改成 import fs from '@ohos.file.fs'。
坑 3(最有价值):修好 import 后,被 any 掩盖的隐藏 bug 会暴露
这是我自认为本次最值得的一步。修完上面两个 import,我主动追问了一句:"修好 import 后,原本被 any 掩盖的类型不匹配会暴露",然后去声明文件里逐一核验所有相关 API 签名,挖出 4 处原本根本不报错的隐藏 bug:
fs.OpenMode.WRITE不存在--命名空间成员只有READ_ONLY / WRITE_ONLY / READ_WRITE / CREATE / TRUNC / APPEND。原代码没报错纯粹因为fs是any。改WRITE_ONLY。DownloadConfig没有filename字段--声明里只有filePath?: string。原代码filename: fileName被any放过。改filePath。DownloadTask.on('complete', ...)回调无参数--声明是callback: () => void,原代码写(downloadPath: string) => ...签名不匹配。DownloadTask.on('fail', ...)参数是number--声明是(err: number) => void,原代码写(err: BusinessError) => ...。
后两条要重写 awaitDownloadComplete 为 Promise<void>,回调对齐 SDK 签名,并移除不再用的 BusinessError 导入。
这 4 个 bug 修完,用户重新 Build 才不会再撞新错。编译器之前"放过"它们,不是因为对,是因为类型信息缺失。这个教训我记下了:修完一个类型 import,一定要回头扫一遍同文件里所有用到该类型的调用。
其他小坑(罗列)
PhotoSelectResult.uris不存在,实际属性是photoUris。DocumentViewPicker.select()直接返回Promise<Array<string>>,没有uris包装。Progress的.style()已废弃(@useinstead type),改用构造器Progress({ type: ProgressType.Linear })。List的对齐方法是.alignListItem(),不是.align()。- ArkTS 严格模式不允许
throw e(arkts-limited-throw),要throw e as Error。 resp.result是string | Object | ArrayBuffer联合类型,赋给string要as string。
五、取回钥匙,准备上手机
编译过了之后,用户要 Key。文档里记录了 Key 的有效期,但明确写"key 值不入库"。于是 SSH 上服务器,在服务器本机调密钥管理接口查到了之前创建的那个 Admin Key 的值,跟文档记录吻合。顺手存进项目 memory,下次直接取,不用再翻聊天记录。
接着是打包。当时 signingConfigs: [] 是空的,模拟器能跑但真机必须有签名。用户没配过自动签名,我给了流程:DevEco 里 Automatically generate signature(需华为开发者账号)-> 开 USB 调试 -> Build 出 -signed.hap -> hdc install。
这里用户问了个好问题:"我构建了 hap 和 app,应该用哪个?" 查产物发现两个都是 -unsigned,还没签名。解释:hap 是单模块安装包,hdc install 直接装手机;app 是上架应用市场的整包格式,装不了。自用一律 hap。
还有一个:"所以一定要先插上手机对吗?"--配签名本身不需要手机(云端生成证书),但 HarmonyOS 的 Debug Profile 会绑定允许调试的设备 UDID。如果配签名时没连手机,Profile 里没设备 UDID,debug 包装上去会报设备不匹配。所以建议先连手机再配签名,一次把 UDID 塞进 Profile。
六、伏笔应验:版本号报错
晚上继续,用户报"发布到手机失败了"。读构建日志,错误 00303015 Configuration Error。读 build-profile.json5 发现:
compatibleSdkVersion: "6.1.1(24)" // 比 target 高
targetSdkVersion: "6.1.0(23)"
这就是上午留的第一个伏笔--版本号是推断的。用户中间可能动过版本号,把 compatible 调到了 6.1.1(24),但本地只装了 API 23 的编译 SDK(6.1.1 仅有系统镜像用于模拟器),没有 API 24 的 ets SDK。规则要求 compatible <= target <= 已装SDK,所以必须把 compatible 降回 "6.1.0(23)",让 23 <= 23 <= 23 成立。
上午推断的风险,晚上如实应验。
七、装机:hdc 的路径坑与无线调试
版本号修好,构建出一个已签名的 hap(153K)。hdc list targets 确认设备已连。
然后撞了 hdc 的一个别扭坑:
hdc install "E:/Code/.../entry-default-signed.hap"
# 报:no such file or directory, path:E:\Code\...\E:/Code/.../entry-default-signed.hap
带盘符的正斜杠绝对路径,被 hdc 拼到了 cwd 前面。改用相对路径(cwd 是项目根)就过了:
hdc install "entry/build/default/outputs/default/entry-default-signed.hap"
后来用户想切无线调试。hdc shell ifconfig wlan0 查到手机 WiFi 局域网 IP,用 hdc tconn <手机IP>:<端口> 连上,无线 hdc install 也成功。
八、功能开发:批量上传与长按菜单
装上能跑了,用户提了四个功能:图片批量上传、前端图片直接预览、长按操作、不能预览的显示大文件图标占位。交互方案选了"纯长按菜单"(去掉右侧下载/删除按钮,列表更干净),批量上限 99 张。
坑 4:ArkTS 严格模式拒绝 Array<Button> 赋值给元组
promptAction.showActionMenu 的 buttons 形参类型是元组 [Button, Button?, Button?, Button?, Button?, Button?](1-6 个,至少 1 个必填)。我一开始用 Array<Button>(可能为空),ArkTS 严格校验直接拒:
ArkTS Compiler Error ... Target requires 1 element(s) but source may have fewer.
先试 as 断言,但不确定严格模式放不放行。最后改成确定可行的写法:用固定元组字面量,按"是否图片 × 是否公开"4 种组合构造 buttons;actions 用平行数组,按下标映射动作。这样字面量直接匹配 SDK 的元组类型。
这里用户明确不满了:"有报错了,你不能先上网查一下他的 sdk 用法再写吗"。说得对。之后改成先查 @ohos.promptAction.d.ts 类型定义,再动代码。
坑 5(最折腾):我删了 IDE 缓存,把 DevEco 也搞崩了
排查命令行 hvigorw.bat assembleHap 失败时,我执行了 rm -rf ".../project_caches/.../workspace",想清缓存重试。结果命令行没修好,反而把 DevEco 用的 hvigor workspace 缓存也删了。DevEco 重建了 workspace 目录,但 @ohos/hvigor 包目录是空的(没 package.json、没 index.js),第二次 DevEco 构建直接报:
worker[14] encounters error: Cannot find module '@ohos/hvigor'
BUILD FAILED in 2 s 807 ms
自伤。修复过程相当曲折,卡在 WSL 调 Windows 命令的引号传递上:
- WSL
cp从 DevEco 内置目录复制 -> 太慢(drvfs 跨盘复制通病),300s 超时没完。 cmd.exe /c robocopy ...-> bash 单引号传给 cmd.exe 时双引号没被正确解析,robocopy 把 cwd 拼到路径前,参数全错,目标 0 文件。- bash 双引号 +
\"转义 -> cmd.exe 引号规则和 bash 冲突,仍失败。 - 最终
powershell.exe -NoProfile -Command "robocopy '源' '目标'"-> 成功,上万文件全到位。
教训两条:不要乱删 IDE 的缓存目录;WSL 调 Windows 命令传参,PowerShell 最稳。
坑 6:显示层把模板插值渲染没了,差点冤枉好代码
这个坑特别有意思,我写这篇博文时又亲手踩了一遍。
开发长按菜单时,我读 ApiConfig.ets,看到下载/删除的 URL 模板里有一段插值,显示出来像是 id 参数根本没用上、URL 是坏的。我当时信心满满地写了一整段修复方案。
幸好多了个心眼,用 od -c 做了字节级检查:
$ sed -n '27p' entry/src/main/ets/services/ApiConfig.ets | od -An -c
... ` $ { A p i C o n f i g . A P I
_ F I L E S } / $ { i d } ` , ...
字节是 24 7b 69 64 7d = $ { i d }。代码本来就是正常的 $glm_5.2_ark_toC 模板插值,是 Read/sed 这类工具的显示层把插值渲染没了,显示成误导形态。撤销修复计划,代码无需改动。
最妙的是,写这篇博文前我再次用 Read 读这个文件,又看到了被误导的形态,差点第二次上当。od -c / hexdump 字节级验证才救了我。教训:对可疑的字符串污染,先 od -c 看字节再下结论。
九、最后一个 bug:中文文件名
功能都写完,用户报"ui 上中文显示有问题"。我第一反应是查文件编码--UTF-8,无 BOM,中文字节正常,没问题。用户一句话点醒:"是 url 解码的问题"。
grep 全项目,只有 ApiConfig.ets 对 key 做了 encodeURIComponent,全文没有任何 decodeURIComponent。根因:picker 返回的 URI 里,中文文件名是百分号编码(如 %E4%B8%AD),extractFileName 提取文件名时没解码,上传到服务器再回显到 UI,就成了编码串。
修复:extractFileName 提取后加 decodeURIComponent;新增 displayName() 方法,用 try-catch 防误解码(兼容历史已上传的编码文件名);列表项、长按菜单标题、删除确认、下载进度 4 处全部改用 displayName(file)。
// 从 URI 提取文件名并解码
extractFileName(uri: string): string {
const idx = uri.lastIndexOf('/');
const raw = idx >= 0 ? uri.substring(idx + 1) : uri;
try { return decodeURIComponent(raw); } catch (e) { return raw; }
}
// 显示用文件名:兼容历史已上传的百分号编码文件名
displayName(file: FileItem): string {
try { return decodeURIComponent(file.fileName); } catch (e) { return file.fileName; }
}
十、复盘:几条教训
一天下来,从空白模板走到真机可用,几条真切的教训:
不靠记忆写 SDK 调用,查
.d.ts声明。鸿蒙 Kit 重组频繁(RequestKit并入BasicServicesKit、fs不在CoreFileKit),记忆里的写法换个 SDK 版本就错。先查声明再写,比写完被编译器打回来快。修完一个类型 import,回头扫同文件所有调用。
any会掩盖类型不匹配,import 修好后这些隐藏 bug 才暴露。主动挖,比被动等下一次 Build 报错强。不乱删 IDE 缓存目录。命令行构建和 IDE 共用 workspace 缓存,删了命令行没修好,还把 IDE 拖下水。WSL 调 Windows 命令传参用 PowerShell 最稳。
可疑字符串污染,先
od -c看字节。显示层可能把模板插值渲染没,看着像 bug 其实是渲染假象。字节不会骗人。版本号别靠推断。
compatibleSdkVersion这种字段,从oh-uni-package.json推断出来一定要 sync 验证,否则早晚应验。真机签名先连手机再配。Debug Profile 绑设备 UDID,先连手机再配自动签名,一次到位,少重签。
Key 不入库,SSH 现查。敏感凭证不入仓库,文档只记有效期,需要时 SSH 到服务器查,查完存项目 memory。
现状与展望
目前 App 在真机上跑通了完整闭环:输入 Key 登录(自动登录)-> 文件列表(图片缩略图/非图片 emoji 占位)-> 从相册/文件批量上传(99 张,带进度)-> 下载(带进度)-> 长按菜单(预览/下载/删除/复制公开链接)-> 退出登录。错误码统一处理,401 自动清 Key 回登录页。
待优化:
- 下载目前落到应用沙箱
filesDir,用户在文件管理器里不直接可见;API 11+ 可用saveas或 FilePicker 中转保存到公共目录。 - Key 明文存
preferences,高安全场景可换huks加密存储。 - 缩略图每次都用下载接口拉全图,大图费流量,可加本地缓存或服务端缩略图接口。
- 正式发布到应用市场需要申请 release 签名/发布证书,目前是 debug 签名自用。
一天,从 Hello World 到真机可用。鸿蒙 @kit.* 系统能力封装本身不难,最费时间的是 SDK 大版本迁移和那几轮编译排错。希望这篇实录对你有帮助。