在鸿蒙 PC 上用 CPF-Ionic 开发鸿蒙应用:一次把坑踩穿的完整实录
用 Web 技术栈写一次代码,同时出 Android、iOS 和鸿蒙三端,是很多团队的现实诉求,但 Capacitor 并不支持 OpenHarmony。CPF-Ionic 把它封装成 hionic 命令行,这篇实录把签名、相机插件到构建的坑一次踩穿。

实测机型:HUAWEI MateBook Pro(HarmonyOS 7.0.0.111,OpenHarmony 7.0.0.109,API 26)
实测工具链:hionic 2.1.16 · Capacitor 8.5.3 · hvigor 7.26.8 · hdc 3.2.0f · Node v26.12.0
成品:一个 React + Vite + Capacitor 的鸿蒙应用,含相机插件,已签名并安装在本机运行
一、为什么要在鸿蒙 PC 上跑这条路
用 Web 技术栈(HTML/CSS/JavaScript)写一次代码,同时出 Android、iOS、OpenHarmony 三端应用,是很多团队的现实诉求。Ionic + Capacitor 是这条路上最成熟的组合:Capacitor 把 Web 产物塞进原生容器,再通过一层 Bridge 暴露相机、定位、文件等设备能力。
问题是 Capacitor 官方并不支持 OpenHarmony。CPF-Ionic 做的事情可以概括为一句话:**把 Capacitor 鸿蒙化,并把整套流程包装成一个命令行工具 hionic**。它基于 @ionic/cli@2.1.15 分支而来,额外提供了:
- hionic platform add openharmony:一键落地一个可编译的 OpenHarmony 壳工程;
- hionic plugin add xxx –platform openharmony:把插件(Capacitor 插件与 Cordova 插件都支持)的原生实现注入壳工程,并自动改好 CMakeLists.txt、capacitor.plugins.json 等配置文件;
- hionic buildui / copy / sync / buildapp / run:前端构建到装机运行的全流程命令。
在鸿蒙 PC 上做这件事更有意思:开发机和目标设备是同一台机器。编译出的 hap 可以直接 hdc install 装回本机运行,不需要额外准备开发板或模拟器,调试闭环极短。
本文按真实操作的顺序记录全过程,把每一个报错的现场、根因和修法都写清楚。涉及的主要仓库:
| 仓库 | 作用 |
|---|---|
| CPF-Ionic/capacitor-cli
(npm 包名 hionic) |
命令行工具本体,含壳工程模板 |
| CPF-Ionic/openHarmony-capacitor | OpenHarmony 版 Capacitor 运行时(har 模块,即 harmony-capacitor) |
| li_in/openharmony-capacitor-openssl3.5 | 预编译的 openssl 3.5 库与头文件(编译必需) |
| 各 CPF-Ionic/capacitor-* | 插件的鸿蒙原生实现(相机、剪贴板、网络等) |
二、环境全景:先看清手里有什么
动手前先摸清这台鸿蒙 PC 上有哪些工具,后面很多决策都基于这个事实:
| 组件 | 位置 / 版本 | 说明 |
|---|---|---|
| 系统 | HarmonyOS 7.0.0.111,API 26 | const.build.characteristics=default
,即零售版(这一点在第 12 章签名部分很关键) |
| Node / npm | v26.12.0 / 11.20.0(harmonybrew) | 系统上没有独立的 node,由 harmonybrew 提供 |
| hmos-clt | /data/service/hnp/hmos-clt.org/hmos-clt_1.0.0 | 华为命令行工具集:hdc、hvigor、ohpm、codelinter、OpenHarmony SDK |
| OpenHarmony SDK | $DEVECO_SDK_HOME
(同上 /sdk) |
含 ArkTS 编译器、CMake/Ninja、clang、签名工具 |
| hdc | 3.2.0f | 设备连接工具,本机可自连(见第 13 章) |
| hvigor | 7.26.8(hvigorw) | 鸿蒙工程构建工具,相当于 Android 的 Gradle |
| devecocli | @deveco/deveco-cli
(npm 全局) |
命令行版的 DevEco 能力
,含 signature generate 自动签名 |
一句经验:在这台机器上,DevEco Studio 这类 IDE 并不是必需的。hvigorw 负责编译、devecocli 负责签名、hdc 负责安装与运行,命令行的链路是完整的——这是本文能在纯终端环境下走通的基础。
三、原理:一次构建到底发生了什么
先建立整体图景,后面每一步踩坑时才知道它卡在链路的哪一环:
React/Vite 源码 (src/)
│ vite build
▼
dist/ ──(hionic copy openharmony)──► openharmony/entry/src/main/resources/rawfile/www/
│
│ hvigorw assembleHap
│ ├─ ArkTS 编译(entry 模块 + Capacitor 插件)
│ ├─ C++ 原生编译(capacitor 模块 + 插件 + openssl)
│ └─ 资源打包
▼
entry-default-unsigned.hap
│ 签名(hvigor / hap-sign-tool)
▼
entry-default-signed.hap
│ hdc install
▼
设备上的 ArkWeb 加载 rawfile/www/index.html,Capacitor 桥接原生能力
几个关键角色的对应关系,理解了就不会被目录名绕晕:
- Web 工程:pcdemo/(根目录),React + Vite + @capacitor/core + @capacitor/cli;
- 壳工程:pcdemo/openharmony/,标准鸿蒙工程,含两个模块——entry(应用入口,ArkTS + 资源)与 capacitor(Capacitor 运行时 har,来自 npm 包 @capacitor-ohos/ohos);
- 前端产物:pcdemo/dist/,最终被复制进壳工程的 rawfile/www/,由 ArkWeb 加载;
- 中转目录:pcdemo/oh-plugins/,插件原生源码在此留存,再由 hionic 注入 capacitor 模块。
接下来的章节按“装工具 → 建工程 → 加平台 → 编译 → 签名 → 装机运行”推进,每一步都给出可直接复制的命令和踩到的坑。
四、安装 hionic:第一个坑来自 npm 的平台校验
按官方文档,一行命令即可:
npm install -g hionic
在鸿蒙 PC 上它会直接失败:
npm error code EBADPLATFORM
npm error notsup Unsupported platform for systeminformation@5.33.15:
wanted {“os”:”darwin,linux,win32,freebsd,openbsd,netbsd,sunos,android”}
(current: {“os”:”openharmony”})
根因:systeminformation 是 hionic 依赖的 cordova 工具链的传递依赖,它的 package.json 里声明了受支持的 os 白名单,而 Node 在这台机器上报告的平台名是 openharmony,不在白名单里,npm 于是拒绝安装整个依赖树。
先试了但没用:npm 支持 –os / –cpu 覆盖,我试过 npm install -g hionic –os=linux –cpu=arm64,npm 11 的平台判断不走这个覆盖,报错一模一样。
有效修法:加 –force。npm 会把平台不匹配从“错误”降级为“警告”并继续安装:
npm install -g hionic –force
hionic -v # 2.1.16
这里可以放心用 –force:systeminformation 是纯 JS 包(不加载原生二进制),装上了也不会被这条链路执行到。安装完成后 hionic 落在 npm 全局前缀下(本机是 ~/npm/bin/hionic)。
顺带记住这条经验:在这台机器上,npm 原生包(.node)只有在发布了 openharmony 预编译包时才能用——第八章的 lightningcss 就是反例,同一章的 rolldown 和第八节换上的 esbuild 则是正例。
五、第二个坑:任何 capacitor 命令一启动就崩
装好 CLI 之后,创建项目时执行到 npx cap init 会立刻抛出这样一段栈:
SystemError [ERR_SYSTEM_ERROR]: A system error occurred:
uv_os_get_passwd returned ENOENT (no such file or directory)
at Object.userInfo (node:os:306:11)
at getShell (@ionic/utils-terminal/dist/info.js:16:26)
at Object.<anonymous> (@ionic/utils-terminal/dist/info.js:36:12)
根因:/etc/passwd 是存在的(500 多条),但当前进程的 uid(100)在其中没有条目,于是 Node 的 os.userInfo() 底层 getpwuid 失败并抛 ERR_SYSTEM_ERROR。要命的是 @ionic/utils-terminal 的 getShell() 在模块顶层就调用它取 shell,所以这不是“某个功能不可用”,而是 require 阶段直接炸——npx cap 的任何子命令、任何依赖该模块的 CLI 都跑不起来。我核对过上游最新的 2.3.5,代码完全一样,没有修复版本可升级。
修法:不动 node_modules(改了下次装依赖就丢),用 NODE_OPTIONS 预加载一个兜底补丁,只在该调用失败时用环境变量合成等价值:
// tools/os-userinfo-shim.js
‘use strict’;
const os = require(‘os’);
const originalUserInfo = os.userInfo;
os.userInfo = function userInfo(options) {
try {
return originalUserInfo.call(os, options);
} catch (error) {
if (!error || error.code !== ‘ERR_SYSTEM_ERROR’) throw error;
return {
uid: typeof process.getuid === ‘function’ ? process.getuid() : -1,
gid: typeof process.getgid === ‘function’ ? process.getgid() : -1,
username: process.env.USER || process.env.LOGNAME || ‘user’,
homedir: process.env.HOME || os.homedir(),
shell: process.env.SHELL || ‘/bin/sh’,
};
}
};
写进 ~/.zshrc,新开终端对所有 Node 进程生效:
export NODE_OPTIONS=”–require $HOME/Desktop/ionic/tools/os-userinfo-shim.js”
验证:
npx cap –version # 8.5.3
这个补丁的设计原则是“只在坏掉的环境里起作用”——正常机器上 os.userInfo() 成功,try 分支原样返回,行为与不打补丁完全一致。唯一代价是补丁路径被 ~/.zshrc 引用,移动工程目录时要同步改那一行。
六、创建应用:一条命令,但差三个依赖
hionic 的项目创建命令格式是:
hionic start <框架> <项目目录> <应用包名> <应用名称> [前端模板]
hionic start capacitor pcdemo com.nutpi.pcde pcdemo react
读一下它源码(src/base-cli.js),这条命令实际做了四件事,理解它才能明白为什么半路会断:
- npm create vite pcdemo — –template react –yes —— 用 Vite 脚手架建 Web 工程;
- 往 vite.config.js 注入 base: ‘./’ —— 鸿蒙化的关键一步,产物必须用相对路径引用资源,否则在壳工程里加载不到 CSS/JS;
- npm install —— 只装 Web 依赖(React、Vite 那一套);
- npx cap init pcdemo com.nutpi.pcde —— 生成 Capacitor 配置。
第 4 步会失败:
Initializing Capacitor…
Executing: npx cap init pcdemo com.nutpi.pcde
npm error could not determine executable to run
npx cap command exited with code 1
根因:hionic 不会把 @capacitor/cli 装进你的工程,npx cap 在项目里找不到可执行文件。官方快速上手文档里也专门提醒过:“需检查依赖是否安装完成,包括 @capacitor/cli 和 @capacitor/core。”
修法:把这两个包装进工程再重新初始化:
cd pcdemo
npm install @capacitor/core@8
npm install –save-dev @capacitor/cli@8
npx cap init pcdemo com.nutpi.pcde
版本为什么选 8:壳工程 openHarmony-capacitor 的 README 明确写着“本项目基于 @capacitor/android@8.0.0 开发”,所以 JS 侧用 Capacitor 8 对齐。当时 @capacitor/cli 最新是 8.5.3,要求 Node ≥ 22(本机 v26.12.0,满足)。
还有一个细节坑:cap init 的第三个位置参数(webDir)在 8.5.3 上不生效,它永远写 webDir: “www”,而 Vite 的产物在 dist/。必须手动改 capacitor.config.json:
{
“appId”: “com.nutpi.pcde”,
“appName”: “pcdemo”,
“webDir”: “dist”
}
这不是小事:hionic 的 copy 命令就是读这个 webDir 决定从哪儿拷前端产物,写错了会把空目录复制进壳工程。
这一步做完,pcdemo/ 下应该是:src/、index.html、vite.config.js、capacitor.config.json、package.json、node_modules/。
七、添加 OpenHarmony 平台:壳工程是怎么长出来的
cd pcdemo
hionic platform add openharmony
这一条命令信息量很大,开 verbose 能看到它做了什么:
detect result: Capacitor confidence: 70%
1. Found Capacitor packages: @capacitor/core, @capacitor/cli
2. Found Capacitor config file: capacitor.config.json
3. Capacitor CLI available
Using hionic openharmony platform support
verbose: Getting template directory…
verbose: Using template directory: …/hionic/templates/capacitor
verbose: Template files copied successfully
log: Copying Capacitor source code…
verbose: Capacitor root: …/pcdemo/node_modules/@capacitor-ohos/ohos
verbose: Capacitor target: …/pcdemo/openharmony/capacitor
verbose: Processed 48 template files
log: Project created successfully
拆开看是三件事:
- 识别框架:靠 package.json 里的 @capacitor/* 依赖 + capacitor.config.json 的存在,所以前面那步 cap init 必须成功,否则这里会判定失败;
- 落地壳工程模板:把 hionic/templates/capacitor/(一个标准鸿蒙工程骨架)复制成 openharmony/;
- 拷入 Capacitor 原生运行时:从 node_modules/@capacitor-ohos/ohos(npm 包 @capacitor-ohos/ohos,在工程里就是 harmony-capacitor har 模块)拷到 openharmony/capacitor/,再对 48 个模板文件做变量替换(bundleName、应用名等)。
完成后目录长这样:
pcdemo/
├── capacitor.config.json # webDir=dist
├── dist/ # vite 产物
├── src/ # React 源码
├── oh-plugins/ # 插件原生源码中转(装插件后才有)
└── openharmony/ # 壳工程
├── AppScope/app.json5 # bundleName / 应用名
├── build-profile.json5 # products + modules(entry + capacitor)
├── entry/ # 应用入口模块(ArkTS + 资源)
│ └── src/main/resources/rawfile/www/ # ← 前端产物最终落地处
├── capacitor/ # Capacitor 运行时 har(源码 + C++ 层)
├── oh-package.json5
├── hvigor/ hvigorfile.ts local.properties
三个值得核对的点:
- AppScope/app.json5 的 bundleName 已经同步成 com.nutpi.pcde(hionic 会从 capacitor 配置回写,不用手改);
- build-profile.json5 的 modules 里 entry 和 capacitor 两个模块都在,缺了 capacitor 就会编译不到运行时;
- entry/oh-package.json5 里有 “harmony-capacitor”: “file:../capacitor”,这就是模块间的引用关系。
八、第三个坑:Vite 构建死在 lightningcss 上
前端构建:
hionic buildui # 内部就是 npm run build
结果直接炸:
[plugin vite:css-post]
Error: [lightningcss minify] Cannot find module ‘../lightningcss.openharmony-arm64.node’
Require stack:
– node_modules/lightningcss/node/index.js
这个坑值得讲透,因为它代表了一整类问题:
- Vite 8 默认用 lightningcss(Rust 写的原生模块)压缩 CSS;
- napi 的原生模块解析规则是按平台拼包名:lightningcss-${process.platform}-${process.arch},在鸿蒙 PC 上就是 lightningcss-openharmony-arm64;
- 这个包在 npm 上不存在(404)。napi 于是退一步找本地文件 ../lightningcss.openharmony-arm64.node,同样没有;
- 我试过装 Linux 版 lightningcss-linux-arm64-gnu 再手工放到期望路径,加载时报 Permission denied——这台机器的沙箱内核不会加载这种 Linux 原生库(不是缺可执行位,chmod +x 之后报错不变)。
一个重要的反证:Vite 8 的打包器 rolldown 同样是原生模块,它却能正常工作,因为 rolldown 官方发布了 @rolldown/binding-openharmony-arm64。所以结论要修正为:不是原生模块不能用,而是“只有发布了 openharmony 预编译包的库才能用”。
修法:换用同样有鸿蒙预编译包的 esbuild 来压缩 CSS:
npm install –save-dev esbuild # 会自动装上 @esbuild/openharmony-arm64
// vite.config.js
import react from ‘@vitejs/plugin-react’
import { defineConfig } from ‘vite’
export default defineConfig({
base: ‘./’,
build: {
// 默认的 lightningcss 在鸿蒙上没有可用二进制,改用有鸿蒙包的 esbuild
cssMinify: ‘esbuild’,
},
plugins: [react()],
})
再跑 hionic buildui:
✓ 20 modules transformed.
dist/index.html 0.45 kB
dist/assets/index-BmnPcQOv.css 4.14 kB │ gzip: 1.49 kB
dist/assets/index-Fp-_Ac56.js 222.52 kB │ gzip: 69.29 kB
✓ built in 2.62s
产物 index.html 里的引用全部是 ./assets/… 相对路径——这正是前面 base: ‘./’ 起作用的地方,换成绝对路径在壳工程里会白屏。
九、把前端产物同步进壳工程
hionic copy openharmony
# Resolved webDir: dist -> …/pcdemo/dist
# Doing copy openharmony platform success
执行后 openharmony/entry/src/main/resources/rawfile/www/ 下会出现 index.html、assets/,以及框架自动注入的 cordova.js、native-bridge.js 等。
三个相关命令的区别容易混,一次说清:
| 命令 | 做什么 |
|---|---|
| hionic buildui | 只跑前端构建(等同 npm run build),产出 dist/ |
| hionic copy openharmony | 把 dist/(按 webDir)拷进壳工程 rawfile/www/,并注入 bridge 相关 js |
| hionic sync openharmony | copy 的全部动作,外加插件与动态资源同步,日常改完插件用这个 |
这里能看出 webDir 为什么必须改对:copy 就是按 capacitor.config.json 里的 webDir 找源目录,写错就会把一个空目录(或 www/ 不存在)复制进去,后面装了应用打开是一片空白。
十、编译 hap:buildapp是 Windows 专用命令
按文档执行:
hionic buildapp openharmony
实测结果:
Use the “hvigorw” command tool of openharmony to build app
Building openharmony app…
Checking DevEco environment variables…
✓ DEVECO_SDK_HOME: /data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/sdk
Failed to build openharmony app: Environment variable DEVECO_IDE_PATH is not configured
这台机器只有 hmos-clt 命令行工具集、没有装 DevEco Studio,所以 DEVECO_IDE_PATH 无从设置。但读源码会发现,即便设了这个变量也跑不起来——它拼出的构建命令是:
“${DEVECO_IDE_PATH}/tools/node/node.exe” \
“${DEVECO_IDE_PATH}/tools/hvigor/bin/hvigorw.js” clean –mode module -p product=default assembleHap …
node.exe 这个文件名和 tools/ 的目录布局都是 DevEco Studio Windows 版的特征,在 Linux/鸿蒙上不可能执行。文档里那句“限定 windows 平台”,说的就是这段硬编码。hionic 的本意就是调 hvigorw,那就直接用 hmos-clt 自带的 hvigorw 执行等价任务:
cd pcdemo/openharmony
hvigorw clean –mode module -p product=default assembleHap \
–analyze=normal –parallel –incremental –no-daemon
DEVECO_SDK_HOME 由 hmos-clt 的环境变量提供(没有的话自行 export 到 SDK 目录即可)。
十一、第四个坑:ninja 找不到 libssl.so.3
第一次编译,26 个任务里 25 个通过,最后死在原生编译上:
> hvigor ERROR: Failed :capacitor:default@BuildNativeWithNinja…
ninja: error: ‘…/openharmony/capacitor/libs/arm64-v8a/libssl.so.3’,
needed by ‘…/libcapacitor.so’, missing and no known rule to make it
根因:Capacitor 鸿蒙运行时的 C++ 层(HTTP 通信、MD5 缓存、TLS 等)依赖 openssl 3.5,而 capacitor/src/main/cpp/CMakeLists.txt 把路径写死了:
set(OPENSSL_LIB_PATH ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH})
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/openssl/${OHOS_ARCH}/include)
…
${OPENSSL_LIB_PATH}/libssl.so.3
${OPENSSL_LIB_PATH}/libcrypto.so.3
也就是这两个目录必须存在(NATIVERENDER_ROOT_PATH 是 capacitor/src/main/cpp,往上三级就是 capacitor/libs):
- 动态库:capacitor/libs/<abi>/{libssl.so.3, libcrypto.so.3}
- 头文件:capacitor/src/main/cpp/openssl/<abi>/include/(源码里有 #include <openssl/ssl.h>、<openssl/md5.h> 等)
官方 FAQ 第一问就是它,并指向预编译仓库 li_in/openharmony-capacitor-openssl3.5。该仓库自述:“使用 openssl 3.5 在 OpenHarmony 系统交叉编译后的二进制文件,包含动态库、静态库和头文件,直接下载后可以不做任何修改直接集成到 Capacitor 的鸿蒙化工程中。”
修法:直接取预编译二进制,省掉自己交叉编译 openssl(那要拉 OpenHarmony SDK 与 openssl 源码,编译 10~30 分钟):
# 预编译仓库含 arm64-v8a 与 x86_64 两套 libs + headers
SRC=openharmony-capacitor-openssl3.5 # 仓库克隆目录
DST=pcdemo/openharmony/capacitor
cp -r $SRC/libs/{arm64-v8a,x86_64} $DST/libs/
cp -r $SRC/openssl/{arm64-v8a,x86_64} $DST/src/main/cpp/openssl/
再编译:
> hvigor BUILD SUCCESSFUL in 48 s
产物:entry/build/default/outputs/default/entry-default-unsigned.hap 25 MB
两个容易忽略的点:
- x86_64 也必须拷。默认构建会同时编 arm64-v8a 与 x86_64 两个 ABI(真机用前者、模拟器用后者),只拷 arm64 会先过一半、再在 x86_64 上死一次,白白多等一轮;
- **重新执行 hionic platform add openharmony 会用 npm 包重刷整个 openharmony/capacitor**,openssl 这两个目录会被抹掉,需要按上面再拷一遍——这也是建议保留一份预编译仓库源码的原因。
顺手核一下 hap 里到底装了什么(unzip -l entry-default-unsigned.hap):
libs/arm64-v8a/libcapacitor.so 2.0 MB # Capacitor 运行时
libs/arm64-v8a/libssl.so.3 1.5 MB # openssl
libs/arm64-v8a/libcrypto.so.3 7.1 MB
libs/x86_64/… # 另一套 ABI
resources/rawfile/www/index.html # 前端入口
resources/rawfile/www/assets/index-*.js # 前端产物
resources/rawfile/capacitor.config.json
resources/rawfile/capacitor.plugins.json
resources/rawfile/config.xml
前端、运行时、openssl、插件清单四样齐了,才是一个真正能跑起来的应用。
(插一句:这次编译若还见到 entry:default@CompileResource 报 Cannot read properties of undefined (reading ‘getName’),那是原生编译失败的连带现象,openssl 到位后自动消失,不用单独修。)
到这一步拿到的还是 unsigned hap,签名是下一章的事。
十二、签名:社区证书在零售版鸿蒙上一定失败
未签名的原因 hvigor 已经提示了:
hvigor WARN: Will skip sign ‘hos_hap’. No signingConfigs profile is configured in current project.
openharmony/build-profile.json5 里 signingConfigs 是空数组、而 products 里引用了 “signingConfig”: “default”,找不到配置就跳过签名。
12.1 先用 SDK 自带的社区材料试一次
OpenHarmony SDK(<SDK>/toolchains/lib/)自带一套开源签名材料:
| 文件 | 用途 |
|---|---|
| OpenHarmony.p12 | keystore(密码惯例是 123456) |
| OpenHarmonyApplication.pem | 应用证书链 |
| OpenHarmonyProfileDebug.pem
/ OpenHarmonyProfileRelease.pem |
profile 签名证书 |
| UnsgnedDebugProfileTemplate.json | profile 模板 |
| hap-sign-tool | 签名工具,原生 ELF 二进制,不需要 java(没有 JRE 的机器很关键) |
流程是「先签 profile,再用 profile 签 hap」。签 profile 前要改模板三处,缺一处都会被设备拒:
- bundle-info.bundle-name → 改成自己的包名;
- debug-info.device-ids → 填 hdc shell bm get –udid 拿到的 UDID(debug profile 只允许装到列出的设备);
- validity → 模板里的有效期是 2021~2024,早已过期,要改成当前时间窗。
L=<SDK>/toolchains/lib
# 1) 签 profile
$L/hap-sign-tool sign-profile -mode localSign \
-keyAlias “openharmony application profile debug” -keyPwd 123456 \
-signAlg SHA256withECDSA -profileCertFile signature/OpenHarmonyProfileDebug.pem \
-inFile signature/pcdemo-debug-profile.json \
-keystoreFile signature/OpenHarmony.p12 -keystorePwd 123456 \
-outFile signature/pcdemo-debug-profile.p7b
# 2) 用 profile 签 hap
$L/hap-sign-tool sign-app -mode localSign \
-keyAlias “openharmony application release” -keyPwd 123456 \
-signAlg SHA256withECDSA -appCertFile signature/OpenHarmonyApplication.pem \
-profileFile signature/pcdemo-debug-profile.p7b \
-inFile entry/build/default/outputs/default/entry-default-unsigned.hap \
-keystoreFile signature/OpenHarmony.p12 -keystorePwd 123456 \
-outFile entry/build/default/outputs/default/entry-default-signed.hap
# 3) 本地校验
$L/hap-sign-tool verify-profile -inFile signature/pcdemo-debug-profile.p7b
$L/hap-sign-tool verify-app -inFile entry-default-signed.hap \
-outCertChain /tmp/a.cer -outProfile /tmp/a.p7b
本地两种校验全都通过(verify-profile success、hap verify successed!),profile 里的包名、有效期、设备白名单也都改对了。看起来一切正常。
12.2 但装到零售版鸿蒙上会被拒
hdc install -r entry-default-signed.hap
# → code:9568257 error: fail to verify pkcs7 file
换 hdc shell “bm install -p …” 走设备侧安装,错误码一模一样。真正的原因在设备日志里:
E C011FE/foundation/HapVerify: [hap_cert_verify_openssl_utils.cpp(GetCertsChain:322)]
it do not come from trusted root,
issuer: C=CN, O=OpenHarmony, OU=OpenHarmony Team, CN=OpenHarmony Application Root CA
E C011FE/foundation/HapVerify: get cert chain for signInfo failed
E C011121/foundation/BMSInstaller: hap file verify failed
C011FE/foundation/HapVerify 就是设备上的 hap 校验模块,它说得很直白:证书链的根不在设备信任列表里。零售版/商业版 HarmonyOS 只信任华为签发的证书链,SDK 里这套是 OpenHarmony 社区自签的,只能装到 OpenHarmony 设备或模拟器上。本机 const.build.characteristics 是 default,即零售版,所以此路不通。
这一点解释了一个常见困惑:“我完全照着官方签名流程做了,为什么装不上?”——签名流程本身没错,错在信任根不同。同理,Linux 版 lightningcss 那次失败也是环境差异而非工具报错,排查时先分清“我操作错了”和“这台机器就是这个约束”。
12.3 正解:用华为签发的调试证书
DevEco Studio 的「自动签名」会向华为侧申请一张调试证书与一份绑定“包名 + 设备”的 profile。这一步不需要开 IDE:devecocli(npm 包 @deveco/deveco-cli)提供命令行版本。
# 1) 登录。必须在真实终端执行:它要求”按 Enter 打开浏览器”,
# 非交互 shell 里 stdin 立即 EOF,命令会直接退出。
devecocli auth login
# 若无法自动打开浏览器,它会打印一条授权链接,手动在浏览器打开并同意即可
devecocli auth status # → Current user: xxx
# 2) 自动签名:按工程当前 bundleName 生成证书/profile,并写回 build-profile.json5
cd pcdemo/openharmony
devecocli signature generate
# Start generating p12 → csr → certificate → profile
# Signing config written to …/openharmony/build-profile.json5
# Signature generation completed successfully.
材料落在 ~/.ohos/config/default_openharmony_*.{p12,cer,csr,p7b}。生成后可以直接验一下 profile:
$L/hap-sign-tool verify-profile -inFile ~/.ohos/config/default_openharmony_*.p7b
# “type”:”debug” “issuer”:”app_gallery”
# “bundle-name”:”com.nutpi.pcde”
# debug-info.device-ids 里含本机 UDID
issuer: app_gallery 说明这是华为侧签发的,设备信任;包名与设备白名单也都对上了。
有一个容易踩的细节:写回 build-profile.json5 的 storePassword / keyPassword 是形如 0000001b5ddf3b… 的 DevEco 加密串,只有 DevEco/hvigor 能解密。别把它改成明文,也别想着自己用 hap-sign-tool 直接吃这套材料——正确姿势就是让 hvigor 去签,它自己会解密。
12.4 重新编译即得签名 hap
hvigorw assembleHap –mode module -p product=default –no-daemon
# 产物:entry/build/default/outputs/default/entry-default-signed.hap 26 MB
日志里不再有 skip sign 的警告。
配套的坑:如果 AppScope/app.json5 的 bundleName 与 profile 的包名不一致,hvigor 会直接拒绝:
The bundleName in app.json5/hvigorfile.ts does not match the bundleName in the generated SigningConfigs.
因为 profile 是绑包名的。**换包名就要重跑一次 devecocli signature generate**,这一点很多人第一次会愣住。
十三、装机运行:开发机就是设备
鸿蒙 PC 上最舒服的一点:本机就是调试设备,hdc 直接把自己认出来,不用再准备开发板或模拟器:
hdc list targets
# 127.0.0.1:34953 TCP Connected localhost hdc
hdc shell “param get const.product.name” # HUAWEI MateBook Pro
hdc shell “param get const.ohos.apiversion” # 26
安装并启动:
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
# [Info]App install path:… msg:install bundle successfully.
hdc shell “aa start -b com.nutpi.pcde -a EntryAbility”
# start ability successfully.
aa start 的 -b 是 bundleName、-a 是 abilityName(来自 entry/src/main/module.json5 里的 mainElement)。
13.1 验证:别信“安装成功”,要看日志
安装成功不等于应用跑得起来(例如前端产物没进去、WebView 白屏、原生模块崩溃都不会让 install 失败)。三层验证缺一不可:
# 第一层:进程活着,而且 WebView 相关的子进程也起来了
hdc shell “ps -ef” | grep nutpi
# 20020184 38284 670 0 09:41:28 ? com.nutpi.pcde
# 20100336 38448 5352 0 09:41:28 ? com.nutpi.pcde:gpu
# 20100337 38602 5352 0 09:41:29 ? com.nutpi.pcde:render
# 第二层:Ability 在前台
hdc shell “hilog -x” | grep -iE “foreground|isFocused”
# AI_LLM_SERVICE: this appName is foreground Resume : [com.nutpi.pcde]
# DSched_Service: bundleName: com.nutpi.pcde … abilityName: EntryAbility isFocused: 1
# 第三层(最关键):WebView 真的加载了我们的页面
hdc shell “hilog -x” | grep “read rawfile”
# A00000/com.nutpi.pcde/ss-handler: read rawfile 459 bytes. file:www/index.html
# A00000/com.nutpi.pcde/ss-handler: read rawfile 222525 bytes. file:www/assets/index-Fp-_Ac56.js
# A00000/com.nutpi.pcde/ss-handler: read rawfile 4145 bytes. file:www/assets/index-BmnPcQOv.css
# A00000/com.nutpi.pcde/ss-handler: read rawfile 13057 bytes. file:www/assets/hero-CLDdwZDr.png
# ……(vite/react 图标、favicon 等)
read rawfile … file:www/index.html 这行是 ArkWeb 从壳工程 rawfile/www/ 读取页面的直接证据;紧随其后的一串 JS/CSS/图片说明静态资源全部到位、base: ‘./’ 的相对路径也生效了。
顺带一个实用技巧:hilog 里 ArkWeb/Native 的调试噪音(chromium、vsync、GPU)极多,过滤时用白名单比黑名单有效:
hdc shell “hilog -x” | grep “nutpi.pcde” | grep -viE “chromium|:gpu|:render”
这样能只看 ArkTS 侧和业务相关的日志。查崩溃、看插件调用是否真的进到原生层,都靠这个。
13.2 完整链路回顾
到这里整条链路闭环,把这篇文章里的命令串起来就是:
React 源码 ──vite build──► dist/ ──hionic copy──► openharmony/entry/src/main/resources/rawfile/www/
│
└──hvigorw assembleHap──► build-profile.json5 里的 signingConfigs(华为调试证书)
│
▼
entry-default-signed.hap ──hdc install──► 本机
│
aa start ──► ArkWeb 加载 rawfile/www/index.html
│
Capacitor Bridge ──► 原生能力(相机/文件/网络…)
十四、插件集成:以相机为例
插件是 Capacitor 的灵魂,也是 hionic 最省事的地方——它把“改原生工程”这堆脏活自动化了。
hionic plugin add @capacitor/camera –platform openharmony
它实际做了六件事(对照实测日志):
1)装 JS 侧@capacitor/camera(8.2.6),这是前端 import 的 API;
2)装 鸿蒙原生实现@capacitor-ohos/camera(8.0.2),源码落到 pcdemo/oh-plugins/camera/;
3)把 ArkTS 源码注入 Capacitor 运行时模块:
- openharmony/capacitor/src/main/ets/components/ImageCompress/(6 个文件)
- openharmony/capacitor/src/main/ets/components/MediaAction/(4 个文件)
4)往 openharmony/capacitor/src/main/cpp/CMakeLists.txt 追加 add_subdirectory(Camera);
5)往 openharmony/entry/src/main/resources/rawfile/capacitor.plugins.json 追加插件登记:
[
{ “pkg”: “@capacitor/CapacitorPlugin”, “classpath”: “CapacitorPlugin” },
{ “pkg”: “@capacitor/camera”, “classpath”: “Camera” }
]
- 更新 package.json(多出 @capacitor/camera 与 @capacitor-ohos/camera 两条依赖)。
验证插件真的落地了:
hionic plugin ls
# openharmony plugins:
# @capacitor-ohos/camera 8.0.2
grep add_subdirectory openharmony/capacitor/src/main/cpp/CMakeLists.txt
# add_subdirectory(getcapacitor)
# add_subdirectory(HotCodePushPlugin)
# add_subdirectory(Camera) ← 新注入
两个容易踩的点:
- 插件原生代码注入的是 capacitor 模块,不是 entry 模块。所以 build-profile.json5 的 modules 不需要新增条目,别照着某些文档去改它;capacitor.plugins.json 和 CMakeLists.txt 才是真正的登记处。
- 安装过程里会出现一行 /bin/sh: cordova: inaccessible or not found,那是 hionic 顺手探测 cordova 环境失败,capacitor 工程可以直接忽略。
前端调用(React 写法):
import { Camera, CameraResultType } from ‘@capacitor/camera’
const takePhoto = async () => {
const photo = await Camera.getPhoto({
quality: 90,
resultType: CameraResultType.Uri,
})
console.log(photo.webPath)
}
需要说明的是:本次实测只验证到“插件已完整集成并打进 hap”(CMakeLists 有 add_subdirectory(Camera)、capacitor.plugins.json 有登记、hap 里含对应原生代码),运行时拉起相机的链路没有实测——那需要在首页加一个调用按钮,并在壳工程里声明权限。编译期插件源码会给出权限提示,例如:
ArkTS:WARN File: …/MediaAction.ets:154:26
To use this API, you need to apply for the permissions: ohos.permission.READ_IMAGEVIDEO
这类系统权限还需要在 entry/src/main/module.json5 的 requestPermissions 里声明、并在运行时申请,不能只靠插件自带。
十五、日常迭代流
改一次前端代码到看到效果,最短路径是这五步:
cd pcdemo
hionic buildui && hionic copy openharmony # 构建前端 + 同步进壳工程
cd openharmony
hvigorw assembleHap –mode module -p product=default –no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell “aa start -b com.nutpi.pcde -a EntryAbility”
打包成一键脚本更省事:
#!/bin/sh
# tools/run-on-device.sh —— 重建并打到同时是本机设备的设备上
set -e
cd “$(dirname “$0″)/../pcdemo”
hionic buildui && hionic copy openharmony
cd openharmony
hvigorw assembleHap –mode module -p product=default –no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell “aa start -b com.nutpi.pcde -a EntryAbility”
只调 UI 样式时,用浏览器预览快得多,不必等编译:
npm run dev # http://localhost:5173
但要清楚边界:浏览器预览里没有原生插件。凡是涉及相机、文件系统、通知的代码,最终都必须回到真机验证——这是 Capacitor 这类“混合容器”的通病,不只是鸿蒙。
十六、速查表
| 用途 | 命令 | 本机特有注意事项 |
|---|---|---|
| 安装 CLI | npm install -g hionic –force | 不加 –force 会 EBADPLATFORM |
| 任何 capacitor 命令 | 前置 NODE_OPTIONS=–require …/os-userinfo-shim.js | 已写进 ~/.zshrc |
| 创建应用 | hionic start capacitor pcdemo com.nutpi.pcde pcdemo react | 之后须手动装 @capacitor/cli@8 + @capacitor/core@8 |
| 添加平台 | hionic platform add openharmony | 会重刷 openharmony/capacitor,openssl 需重拷 |
| 添加插件 | hionic plugin add @capacitor/camera –platform openharmony | 注入 capacitor 模块 |
| 构建前端 | hionic buildui | 依赖 build.cssMinify: ‘esbuild’ |
| 同步产物 | hionic copy openharmony | 源目录由 webDir 决定(必须是 dist) |
| 编译 hap | cd openharmony && hvigorw assembleHap –mode module -p product=default –no-daemon | hionic buildapp
是 Windows 专用 |
| 自动签名 | devecocli signature generate | 需先 devecocli auth login |
| 安装 | hdc install -r …/entry-default-signed.hap | |
| 启动 | hdc shell “aa start -b com.nutpi.pcde -a EntryAbility” | |
| 看日志 | hdc shell “hilog -x” | grep com.nutpi.pcde | 再滤掉 chromium|:gpu|:render |
| 设备 UDID | hdc shell bm get –udid | 签 profile 要用 |
| 前端预览 | npm run dev | 无原生能力 |
十七、FAQ 与经验总结
17.1 常见问题
Q1:为什么不直接用 DevEco Studio?
不是不行,而是不必。命令行链路本身是完整的(hvigorw 编译 + devecocli 签名 + hdc 安装),适合没装 IDE 的鸿蒙 PC,也更容易接进 CI。DevEco 唯一不好替代的是图形化工程配置向导,而本项目里那些配置,hionic 和 devecocli 已经代劳了。
Q2:应用装上了,打开白屏怎么办?
按这个顺序查,基本能命中:
- capacitor.config.json 的 webDir 是不是 dist;
- hionic copy openharmony 之后,openharmony/entry/src/main/resources/rawfile/www/index.html 在不在;
- vite.config.js 有没有 base: ‘./’(绝对路径会让资源 404);
- hdc shell “hilog -x” | grep “read rawfile” 有没有 index.html 那行——有,说明资源加载正常、问题在页面自身的 JS 报错,转去浏览器 npm run dev 复现更快。
Q3:换包名要动哪些地方?
openharmony/AppScope/app.json5 的 bundleName + capacitor.config.json 的 appId → 重跑 devecocli signature generate(profile 绑包名)→ 重编重装。启动命令里的 -b 也要跟着换。
Q4:这个 hap 能发给别人装吗?
不能。这里签的是 debug 证书,profile 只允许列出的设备(含本机 UDID)安装,本质是调试产物。要发布必须走华为的正式签名与上架流程。
Q5:为什么有些坑在用 DevEco 的 Windows 上完全遇不到?
差异集中在三处,都是“环境级”而不是“框架级”:
- npm 把平台识别成 openharmony,导致原生预编译包缺失(lightningcss)与平台校验失败(systeminformation);
- 零售版鸿蒙只信任华为签发的证书链,SDK 里的社区签名材料注定被拒;
- hionic 的 buildapp 命令硬编码了 Windows 版 DevEco 的目录布局。
除此之外,工具链行为与 Windows 上一致。
17.2 经验总结
这趟下来最值得说的不是某条命令,而是排障方法:
- 先分清“我用错了”和“环境不允许”。 三次卡死(npm 平台校验、lightningcss 加载、社区证书被拒)都不是操作失误,而是这台机器的既定约束。看清这点,才能停止在错误方向上折腾,转去找环境允许的替代方案(–force、esbuild、华为证书)。
- 读源码比读文档快。 hionic 本体只有 8000 多行 JS,卡住时直接看 src/base-cli.js 和 src/utils/*:buildapp 为什么是 Windows 专用、copy 按哪个目录拷、插件注入到哪个模块,答案都在代码里。
- 让设备日志当裁判。code:9568257 fail to verify pkcs7 file 只是结论,HapVerify: it do not come from trusted root, issuer: … 才是原因。安装失败、白屏、崩溃,最后一招永远是 hilog。
- 验证要分层,别停在“命令没报错”。 编译成功 ≠ 能装,安装成功 ≠ 能跑,进程活着 ≠ 页面渲染了。三层证据(进程在跑 / Ability 在前台 / WebView 读到了页面)齐了,才算真的跑起来。
17.3 参考
| 资源 | 地址 |
|---|---|
| hionic(CPF-Ionic capacitor-cli) | https://atomgit.com/CPF-Ionic/capacitor-cli |
| OpenHarmony 版 Capacitor 运行时 | https://atomgit.com/CPF-Ionic/openHarmony-capacitor |
| openssl 3.5 预编译库(编译必需) | https://atomgit.com/li_in/openharmony-capacitor-openssl3.5 |
| 本文对应的完整环境记录 | 同目录 SETUP-NOTES.md |
结语
在鸿蒙 PC 上用 CPF-Ionic 开发鸿蒙应用,链路本身是通的,而且有一个别处没有的好处:开发机就是终端设备。hvigorw 编完直接 hdc install 装回本机,改一行代码到看见效果,全程不到一分钟。
代价是要接受一串“非标准平台”的额外功课:npm 报出的 openharmony 平台名、缺失的原生预编译包、硬编码 Windows 路径的命令,以及零售版鸿蒙的签名信任链。这些坑都不深,但每一个第一次遇到,都足够让人怀疑是不是自己哪里做错了。把它们记下来,后来者就能少走几小时。
一句话版本:
–force 装 CLI,NODE_OPTIONS 过 os.userInfo,esbuild 替 lightningcss,hvigorw 替 buildapp,openssl 记得拷,签名交给 devecocli——剩下的交给 hdc 和 hilog。
本文由人人都是产品经理作者【nutpi】,微信公众号:【nutpi】,原创/授权 发布于人人都是产品经理,未经许可,禁止转载
题图来自Unsplash,基于 CC0 协议。
- 目前还没评论,等你发挥!

起点课堂会员权益



