CHINER 元数建模 Electron 鸿蒙 PC 适配全记录:从普通桌面应用到 HAP 可运行
前言
这篇文章记录一次比较完整的 Electron 应用鸿蒙 PC 适配过程:把原本面向 Windows、macOS、Linux 桌面环境运行的 CHINER 元数建模,一步一步改造成可以在 HarmonyOS / OpenHarmony PC 环境中安装、启动、显示主界面,并完成基础窗口操作的应用。
欢迎加入鸿蒙 PC 开发者社区,共同打造开发者工具生态:
鸿蒙 PC 开发者社区:https://harmonypc.csdn.net/
CHINER-ohos项目开源地址:
https://atomgit.com/OpenHarmonyPCDeveloper/ohos_chiner
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
环境搭建文章:https://blog.csdn.net/lbcyllqj/article/details/161286249?sharetype=blogdetail&sharerId=161286249&sharerefer=PC&sharesource=lbcyllqj&spm=1011.2480.3001.8118
CHINER 本身不是一个简单 demo,而是一个真实的元数据建模工具。它包含项目创建、模板打开、文件读写、图形化建模、导出、配置管理等能力。也正因为它是一个真实 Electron 项目,适配过程中遇到的问题就很典型:
- Electron 主进程如何在鸿蒙 HAP 中被启动
- 原来的
app/build资源如何同步到鸿蒙工程 node_modules运行时依赖如何随包带进去- 桌面端资源路径和鸿蒙包内资源路径如何兼容
@electron/remote在 OpenHarmony Electron 环境下如何补丁- 原桌面窗口控制按钮到了鸿蒙 PC 上如何适配
- 右上角关闭按钮看似存在、实际点击偶尔无响应的问题如何定位
这次适配的目标不是一次性把所有桌面端能力完整复刻,而是先达到一个明确的工程目标:
用户在鸿蒙 PC 上安装后,可以启动 CHINER,看到正常界面,完成基础浏览和项目入口操作,并且窗口最小化、最大化、关闭这些基础交互稳定可用。
一、先看原项目:这是一个典型但不算轻量的 Electron 应用
CHINER 的主体结构仍然是传统 Electron 项目:
chiner-develop-ohos/
├── src/
│ ├── main.js
│ ├── app/
│ ├── components/
│ └── lib/
├── public/
│ ├── jar/
│ ├── file/
│ └── template/
├── scripts/
├── app/
└── package.json
它的桌面端启动链路大致是:
src/main.js创建BrowserWindow- 主进程加载打包后的
index.html - 渲染进程显示首页、模板、项目管理界面
- 通过 Electron IPC 调用文件、导出、更新等能力
迁移到鸿蒙 PC 后,最关键的变化是:应用不再由普通桌面 Electron 安装包启动,而是要被放进 OpenHarmony/HarmonyOS 的 HAP 工程里,由鸿蒙侧的 Electron 运行时拉起。

因此这次适配不是简单改几行 UI,而是需要建立一条新的链路:
原 Electron 项目
-> Webpack/Node 构建出 app/build
-> 同步到 ohos_hap 的 resources/app
-> 生成鸿蒙运行时入口 main.js
-> 安装运行时依赖
-> hvigor 构建 HAP
-> hdc 安装到鸿蒙 PC 环境

二、搭建鸿蒙侧承载工程:不是重写应用,而是增加一个 HAP 壳
第一步是准备 ohos_hap/。它承担的是鸿蒙应用工程的角色,里面包含两个关键模块:
ohos_hap/
├── electron/
└── web_engine/
这里的思路非常明确:CHINER 的业务仍然留在原 Electron 项目里,鸿蒙工程只负责承载和启动。
最终需要把 Electron 运行资源放到:
ohos_hap/web_engine/src/main/resources/resfile/resources/app
这个目录在适配中非常关键。因为 OpenHarmony Electron 启动时会从这里读取应用资源,包括:
main.jspackage.jsonbuild/main.jsbuild/index.htmlbuild/lib/ohos-runtime.js- 运行时
node_modules
换句话说,鸿蒙壳本身只是一层容器,真正的 CHINER 仍然是原来的 Electron 应用,只是入口和资源路径被重新组织了。
三、建立自动同步脚本:避免每次手工搬资源
真实项目适配最怕的一件事是:看起来代码改了,实际打进 HAP 的还是旧资源。
所以我没有依赖手动复制,而是增加了 scripts/build-ohos-package.js,把资源同步流程固化下来。
这个脚本主要做几件事:
- 判断
app/build是否已经是最新 - 如果不是最新,先执行原来的前端构建
- 清理鸿蒙资源输出目录
- 复制
app/build到鸿蒙resources/app/build - 生成鸿蒙环境专用的
main.js - 生成运行时
package.json - 在资源目录安装生产依赖
- 检查必要文件是否齐全
核心入口大概长这样:
ensureBuildArtifacts();
if (isRuntimeOutputFresh()) {
console.log(`CHINER OpenHarmony app resources are up to date: ${outputDir}`);
process.exit(0);
}
ensureCleanOutput();
copyApplicationBuild();
writeBootstrapEntry();
writeRuntimePackageJson();
installRuntimeDependencies();
assertRuntimeApp();
这里最重要的是 writeBootstrapEntry()。它会在鸿蒙资源目录生成一个新的启动入口:
'use strict';
process.env.CHINER_OPENHARMONY = '1';
process.env.CHINER_NODE_ENV = 'production';
process.env.CHINER_DISABLE_GPU = process.env.CHINER_DISABLE_GPU || '1';
require('./build/main.js');
这个入口负责把当前运行环境标记为 OpenHarmony,然后再进入原来的 Electron 主进程。

四、打通 HAP 构建命令:一条命令生成鸿蒙安装包
资源同步之后,还需要触发鸿蒙工程构建。这里增加了 scripts/build-ohos-hap.js,它做了三件事:
- 先调用
build-ohos-package.js同步 CHINER 应用资源 - 找到 DevEco Studio 的
ohpm - 找到
hvigor和 DevEco SDK,执行assembleHap
package.json 中新增了几个命令:
{
"scripts": {
"build:ohos": "node scripts/build-ohos-package.js",
"ohos:sync": "npm run build:ohos",
"ohos:build": "node scripts/build-ohos-hap.js"
}
}
之后完整构建只需要:
npm run ohos:build
成功后会生成:
ohos_hap/electron/build/default/outputs/default/electron-default-unsigned.hap
这个产物就可以通过 hdc 安装到鸿蒙 PC 环境:
HDC=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
$HDC install -r ohos_hap/electron/build/default/outputs/default/electron-default-unsigned.hap
$HDC shell aa start -a EntryAbility -b com.chiner.ohos -m electron

五、处理运行时差异:识别 OpenHarmony 并修正资源路径
原桌面版 Electron 中,资源路径通常可以按桌面安装包结构去推导。但进入鸿蒙 HAP 后,路径变成了类似:
/data/storage/el1/bundle/electron/resources/resfile/resources/app/build
如果仍然沿用桌面端 app.asar.unpacked 的逻辑,很多资源都会找不到。例如:
jar/chiner-java.jarfile/chiner-docx-tpl.docx- 模板文件
- 构建后的前端资源
因此新增了 src/lib/ohos-runtime.js,集中处理运行时判断和资源路径:
const isOpenHarmonyRuntime = (runtime = {}) => {
const env = runtime.env || process.env;
const dirname = runtime.dirname || __dirname;
const resourcesPath = runtime.resourcesPath || process.resourcesPath || '';
return env.CHINER_OPENHARMONY === '1' ||
dirname.includes('/data/storage/el1/bundle/') ||
resourcesPath.includes('/data/storage/el1/bundle/');
};
资源路径也按环境分开:
const getRuntimeResourcePath = (relativePath, runtime = {}) => {
if (env.CHINER_NODE_ENV === 'development') {
return path.join(dirname, '..', 'public', relativePath);
}
if (isOpenHarmonyRuntime({ env, dirname, resourcesPath })) {
return path.join(dirname, relativePath);
}
return path.join(dirname, '../../app.asar.unpacked/build', relativePath);
};
这样,主进程里原来类似 jarPath、docx 的 IPC 逻辑就可以保持稳定,只需要通过 resolveRuntimeResource() 统一拿路径。
六、处理 Electron 主进程:禁用不适合鸿蒙环境的桌面能力
CHINER 原本会用到自动更新能力:
electron-updater
但鸿蒙 HAP 的安装和更新机制与桌面端不同,不能直接沿用桌面安装包的更新逻辑。因此在主进程里做了环境判断:
ipcMain.on('update', () => {
if (isOpenHarmony || !autoUpdater) {
win.webContents.send('updateEnd');
return;
}
autoUpdater.checkForUpdates();
});
这类改动的原则是:不是所有桌面能力都要在鸿蒙第一版里强行迁移。
对于第一阶段基础可用版本来说,优先级应该是:
- 应用能启动
- 页面能渲染
- 基础项目入口可用
- 窗口操作稳定
- 不因为桌面专属能力导致白屏或崩溃
自动更新、系统托盘、全局快捷键、部分桌面原生菜单等能力,可以后续再按鸿蒙平台能力逐个补齐。
七、修复 @electron/remote 在 OpenHarmony 环境下的问题
很多老 Electron 项目会依赖 @electron/remote。CHINER 也一样。
在 OpenHarmony Electron 环境中,@electron/remote 某些逻辑对 Electron 原生 binding 的假设不完全成立,可能导致运行时异常。因此增加了:
scripts/patch-electron-remote-ohos.js
这个补丁做了两类处理。
第一类是让 feature 判断更稳,不直接假设方法一定存在:
const isFeatureEnabled = (checkerName) => {
if (!features) {
return true;
}
const checker = features[checkerName];
return typeof checker === 'function' ? checker.call(features) : false;
};
第二类是在 OpenHarmony 环境下允许 remote module:
if (process.env.CHINER_OPENHARMONY === '1') {
return true;
}
这个脚本会在两个时机执行:
- 根项目依赖补丁
- 鸿蒙资源目录里的运行时依赖补丁
这样可以避免出现“本地依赖修了,但打进 HAP 的依赖没修”的情况。
八、窗口壳适配:为什么最后选择自定义鸿蒙窗口按钮
最开始适配窗口时,一个直觉方案是:既然鸿蒙 PC 有自己的窗口标题栏,那 Electron 直接使用 native frame,是不是就能显示系统的最小化、最大化、关闭按钮?
实际验证后发现,这条路不稳定。当前环境下开启 native frame 并不能得到预期的鸿蒙 PC 系统按钮,右上角按钮缺失或者表现不符合鸿蒙 PC 应用预期。
所以最后采用的是:
frame: false
然后在 CHINER 自己的 Toolbar 中,针对 OpenHarmony 运行时显示一套鸿蒙 PC 风格的窗口控制按钮。
主进程窗口参数集中在 src/lib/ohos-runtime.js:
const getMainWindowOptions = ({ isOpenHarmony = false } = {}) => ({
width: 1180,
height: 600,
minWidth: 1180,
minHeight: 600,
frame: false,
resizable: isOpenHarmony,
minimizable: true,
maximizable: true,
closable: true,
show: false,
title: APP_TITLE,
backgroundColor: isOpenHarmony ? '#ffffff' : 'transparent',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true,
},
});
渲染进程通过 IPC 获取运行时信息:
ipcMain.on(RUNTIME_INFO_CHANNEL, (event) => {
event.returnValue = {
isOpenHarmony,
nativeFrame: false,
harmonyWindowControls: isOpenHarmony,
};
});
首页再把 harmonyWindowControls 传给 Toolbar:
<ToolBar
resizeable
harmonyControls={harmonyWindowControls}
title={<FormatMessage id='system.title'/>}
/>
这样桌面端仍然保持原来的窗口逻辑,鸿蒙 PC 则走单独的按钮样式和事件处理。
九、真正的坑:关闭按钮看得见,但偶尔要点两次
窗口按钮做出来之后,表面上已经接近可用。但测试时发现一个很烦的问题:
右上角关闭按钮有时第一次点击没有反应,需要点一两次才弹出关闭确认。
这类问题如果只看 React 代码,很容易误判成:
onClick没绑好- Modal 弹窗状态异常
close()IPC 不稳定- 按钮层级不够高
但实际根因更偏系统窗口命中区域。
通过鸿蒙侧 uitest dumpLayout 查看布局后发现,鸿蒙 PC 窗口右上角存在一个透明的系统命中区域:
containerModalButtonRowId
bounds: [1777,302][1817,348]
而最初自定义关闭按钮的位置与这个透明区域发生了重叠。结果就是用户点在视觉上的关闭按钮区域时,部分点击可能被鸿蒙系统透明控件吃掉,导致 React 的按钮事件没有稳定触发。
修复分两步。
第一步,把鸿蒙自定义窗口按钮整体左移,避开系统透明命中区:
.@{prefix}-toolbar-opt-ohos {
position: absolute;
top: 0;
right: 48px;
z-index: 3;
display: flex;
align-items: stretch;
height: 32px;
-webkit-app-region: no-drag;
}
修复后布局变成:
关闭按钮: [1704,302][1759,342]
系统透明区: [1777,302][1817,348]
两个区域不再重叠。
第二步,关闭按钮不只依赖 click,而是在 touchstart、mousedown、click 三个阶段都接入同一个关闭逻辑,同时加关闭确认防重复锁:
<button
type='button'
className={`${currentPrefix}-toolbar-opt-ohos-btn ${currentPrefix}-toolbar-opt-ohos-close`}
onTouchStart={(event) => onHarmonyControlClick(event, _close)}
onMouseDown={(event) => onHarmonyControlClick(event, _close)}
onDoubleClick={stopWindowControlEvent}
onClick={(event) => onHarmonyControlClick(event, _close)}
title='关闭'
aria-label='关闭'
>
<Icon type='fa-times'/>
</button>
同时 Modal.confirm 支持 onCancel,这样用户取消关闭后,关闭按钮的防重复锁可以释放:
Modal.confirm({
title: FormatMessage.string({id: 'exitConfirmTitle'}),
message: FormatMessage.string({id: 'exitConfirm'}),
onOk: () => {
close();
},
onCancel: () => {
closingConfirmRef.current = false;
},
});
这个问题很值得记录,因为它不是一个普通前端点击事件 bug,而是 Electron frameless 窗口、鸿蒙 PC 系统透明标题栏区域、Web 按钮命中测试叠在一起之后产生的问题。
十、补测试:不要只靠肉眼觉得“能用了”
适配做到这里,最好补一组基础测试,把关键判断固化下来。
这次新增了 test/ohos-basic-use.test.js,覆盖几个重要点:
- 能识别 OpenHarmony 运行时
- OpenHarmony 主窗口使用自定义鸿蒙窗口控件
- 渲染层能暴露鸿蒙 PC 窗口按钮
- 鸿蒙资源路径能正确解析
- 基础 CHINER 项目 JSON 能写入并读回
- HAP 打包输入文件齐全
- 构建脚本会保留 OpenHarmony runtime helper
其中窗口按钮相关测试会检查:
assert.match(mainSource, /harmonyWindowControls:\s*isOpenHarmony/);
assert.match(runtimeSource, /harmonyWindowControls/);
assert.match(homeSource, /harmonyControls=\{harmonyWindowControls\}/);
assert.match(toolbarSource, /toolbar-opt-ohos/);
assert.match(toolbarSource, /最大化\/还原/);
assert.match(toolbarSource, /最小化/);
assert.match(toolbarSource, /关闭/);
assert.match(toolbarStyle, /toolbar-opt-ohos-close/);
assert.match(toolbarStyle, /right:\s*48px/);
执行:
npm test
结果:
tests 7
pass 7
fail 0
测试不是为了证明所有业务功能都完美,而是为了守住这次适配的底线:鸿蒙运行时识别、资源路径、HAP 打包输入、窗口控件这些基础能力不能在后续改动中被破坏。
十一、最终验证:安装到鸿蒙 PC,首击关闭按钮弹出确认
最后一步一定要在鸿蒙环境里实际验证,而不是只停在构建成功。
我使用的验证命令是:
HDC=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
$HDC uninstall com.chiner.ohos
$HDC install -r ohos_hap/electron/build/default/outputs/default/electron-default-unsigned.hap
$HDC shell aa start -a EntryAbility -b com.chiner.ohos -m electron
启动后用 uitest 点击关闭按钮中心位置:
$HDC shell "uitest uiInput click 1732 322"
验证结果:
- 第一次点击关闭按钮就弹出“关闭确认”
- 点击取消后,再次点击关闭按钮,也能第一次弹出确认
- 日志中没有出现 CHINER 侧新的
Uncaught、did-fail-load或关闭流程异常

十二、这次适配后的可用边界
到这里,CHINER 在鸿蒙 PC 上已经达到了基础可用状态:
- HAP 可以成功构建
- 应用可以安装启动
- 首页可以正常显示
- 参考模板区域可以展示
- 基础资源可以随包带入
- OpenHarmony 运行时可以被识别
- 桌面自动更新逻辑不会干扰鸿蒙运行
- 自定义鸿蒙 PC 窗口按钮可见
- 最小化、最大化、关闭按钮具备基础交互
- 关闭按钮首击可以稳定弹出确认
- 基础测试通过
十三、复盘:Electron 适配鸿蒙 PC 最容易踩的几个点
这次 CHINER 的适配可以总结出几条经验。
第一,构建链路一定要自动化。
不要手工复制 build 目录到鸿蒙工程,后面排查问题时会分不清是代码错了,还是资源没同步。
第二,运行时判断要集中。CHINER_OPENHARMONY、/data/storage/el1/bundle/、process.resourcesPath 这些判断散落到各处会很难维护。集中放到 ohos-runtime.js 后,主进程和测试都更清楚。
第三,桌面端能力要分级迁移。
自动更新、系统菜单、部分原生能力不一定适合第一版鸿蒙 HAP。先让它不阻塞启动,再考虑平台化实现。
第四,Electron frameless 窗口在鸿蒙 PC 上要特别注意系统透明命中区。
视觉上按钮在那里,不代表点击一定能落到 Web 按钮上。uitest dumpLayout 在这类问题上非常有用。
第五,测试用例要覆盖“适配约束”,而不只是业务逻辑。
例如 right: 48px 看起来像一个样式细节,但它背后其实是避免关闭按钮被系统透明区域吃掉的关键约束,应该进入测试保护。
结语
把 CHINER 这种真实 Electron 工具迁到鸿蒙 PC,不是简单地“换个打包命令”就结束了。真正的工作分布在构建、运行时、资源路径、依赖补丁、窗口壳、系统命中区域和验证测试之间。
这次适配最终形成了一条比较清晰的路径:
准备 ohos_hap 承载工程
-> 自动同步 Electron 构建资源
-> 生成 OpenHarmony 启动入口
-> 修正运行时路径
-> 补 @electron/remote 兼容
-> 构建 HAP
-> 安装启动验证
-> 自定义鸿蒙窗口按钮
-> 定位透明命中区问题
-> 补测试并复测关闭首击
最终结果是:CHINER 已经可以作为一个基础可用的鸿蒙 PC Electron 应用运行起来。后面继续扩展文件读写、导出、复杂建模等能力时,也有了稳定的构建和验证底座。
对 Electron 项目迁移鸿蒙来说,这个过程比“一次成功”的故事更有参考价值。因为真实迁移往往就是这样:先让它能活,再让它能用,最后再一点点接近原桌面端体验。
更多推荐



所有评论(0)