前言

这篇文章记录一次比较完整的 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

它的桌面端启动链路大致是:

  1. src/main.js 创建 BrowserWindow
  2. 主进程加载打包后的 index.html
  3. 渲染进程显示首页、模板、项目管理界面
  4. 通过 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.js
  • package.json
  • build/main.js
  • build/index.html
  • build/lib/ohos-runtime.js
  • 运行时 node_modules

换句话说,鸿蒙壳本身只是一层容器,真正的 CHINER 仍然是原来的 Electron 应用,只是入口和资源路径被重新组织了。


三、建立自动同步脚本:避免每次手工搬资源

真实项目适配最怕的一件事是:看起来代码改了,实际打进 HAP 的还是旧资源。

所以我没有依赖手动复制,而是增加了 scripts/build-ohos-package.js,把资源同步流程固化下来。

这个脚本主要做几件事:

  1. 判断 app/build 是否已经是最新
  2. 如果不是最新,先执行原来的前端构建
  3. 清理鸿蒙资源输出目录
  4. 复制 app/build 到鸿蒙 resources/app/build
  5. 生成鸿蒙环境专用的 main.js
  6. 生成运行时 package.json
  7. 在资源目录安装生产依赖
  8. 检查必要文件是否齐全

核心入口大概长这样:

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,它做了三件事:

  1. 先调用 build-ohos-package.js 同步 CHINER 应用资源
  2. 找到 DevEco Studio 的 ohpm
  3. 找到 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.jar
  • file/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);
};

这样,主进程里原来类似 jarPathdocx 的 IPC 逻辑就可以保持稳定,只需要通过 resolveRuntimeResource() 统一拿路径。


六、处理 Electron 主进程:禁用不适合鸿蒙环境的桌面能力

CHINER 原本会用到自动更新能力:

electron-updater

但鸿蒙 HAP 的安装和更新机制与桌面端不同,不能直接沿用桌面安装包的更新逻辑。因此在主进程里做了环境判断:

ipcMain.on('update', () => {
  if (isOpenHarmony || !autoUpdater) {
    win.webContents.send('updateEnd');
    return;
  }
  autoUpdater.checkForUpdates();
});

这类改动的原则是:不是所有桌面能力都要在鸿蒙第一版里强行迁移。

对于第一阶段基础可用版本来说,优先级应该是:

  1. 应用能启动
  2. 页面能渲染
  3. 基础项目入口可用
  4. 窗口操作稳定
  5. 不因为桌面专属能力导致白屏或崩溃

自动更新、系统托盘、全局快捷键、部分桌面原生菜单等能力,可以后续再按鸿蒙平台能力逐个补齐。


七、修复 @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,而是在 touchstartmousedownclick 三个阶段都接入同一个关闭逻辑,同时加关闭确认防重复锁:

<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 侧新的 Uncaughtdid-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 项目迁移鸿蒙来说,这个过程比“一次成功”的故事更有参考价值。因为真实迁移往往就是这样:先让它能活,再让它能用,最后再一点点接近原桌面端体验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐