在鸿蒙 PC 上用 CPF-Ionic 开发鸿蒙应用:一次把坑踩穿的完整实录

0 评论 268 浏览 0 收藏 52 分钟

用 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),这条命令实际做了四件事,理解它才能明白为什么半路会断:

  1. npm create vite pcdemo — –template react –yes —— 用 Vite 脚手架建 Web 工程;
  2. 往 vite.config.js 注入 base: ‘./’ —— 鸿蒙化的关键一步,产物必须用相对路径引用资源,否则在壳工程里加载不到 CSS/JS;
  3. npm install —— 只装 Web 依赖(React、Vite 那一套);
  4. 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

拆开看是三件事:

  1. 识别框架:靠 package.json 里的 @capacitor/* 依赖 + capacitor.config.json 的存在,所以前面那步 cap init 必须成功,否则这里会判定失败;
  2. 落地壳工程模板:把 hionic/templates/capacitor/(一个标准鸿蒙工程骨架)复制成 openharmony/;
  3. 拷入 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

三个值得核对的点:

  1. AppScope/app.json5 的 bundleName 已经同步成 com.nutpi.pcde(hionic 会从 capacitor 配置回写,不用手改);
  2. build-profile.json5 的 modules 里 entry 和 capacitor 两个模块都在,缺了 capacitor 就会编译不到运行时;
  3. 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

这个坑值得讲透,因为它代表了一整类问题:

  1. Vite 8 默认用 lightningcss(Rust 写的原生模块)压缩 CSS;
  2. napi 的原生模块解析规则是按平台拼包名:lightningcss-${process.platform}-${process.arch},在鸿蒙 PC 上就是 lightningcss-openharmony-arm64;
  3. 这个包在 npm 上不存在(404)。napi 于是退一步找本地文件 ../lightningcss.openharmony-arm64.node,同样没有;
  4. 我试过装 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” }

]

  1. 更新 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:应用装上了,打开白屏怎么办?

按这个顺序查,基本能命中:

  1. capacitor.config.json 的 webDir 是不是 dist;
  2. hionic copy openharmony 之后,openharmony/entry/src/main/resources/rawfile/www/index.html 在不在;
  3. vite.config.js 有没有 base: ‘./’(绝对路径会让资源 404);
  4. 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 经验总结

这趟下来最值得说的不是某条命令,而是排障方法:

  1. 先分清“我用错了”和“环境不允许”。 三次卡死(npm 平台校验、lightningcss 加载、社区证书被拒)都不是操作失误,而是这台机器的既定约束。看清这点,才能停止在错误方向上折腾,转去找环境允许的替代方案(–force、esbuild、华为证书)。
  2. 读源码比读文档快。 hionic 本体只有 8000 多行 JS,卡住时直接看 src/base-cli.js 和 src/utils/*:buildapp 为什么是 Windows 专用、copy 按哪个目录拷、插件注入到哪个模块,答案都在代码里。
  3. 让设备日志当裁判。code:9568257 fail to verify pkcs7 file 只是结论,HapVerify: it do not come from trusted root, issuer: … 才是原因。安装失败、白屏、崩溃,最后一招永远是 hilog。
  4. 验证要分层,别停在“命令没报错”。 编译成功 ≠ 能装,安装成功 ≠ 能跑,进程活着 ≠ 页面渲染了。三层证据(进程在跑 / 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 协议。

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
海报
评论
评论请登录
  1. 目前还没评论,等你发挥!