鸿蒙PC:electron-markdownify 从普通 Electron 迁移到 OpenHarmony Electron HAP 的完整实践
本文记录一次把桌面端 Electron 项目electron-markdownify-master迁移成 OpenHarmony Electron 项目的完整过程。迁移目标不是重写 Markdown 编辑器而是在尽量不改业务编辑逻辑的前提下让原来的 Electron 应用可以继续在 macOS 上用npm start跑起来同时也可以被打进鸿蒙 HAP在鸿蒙模拟器上正常启动和显示。此项目开源地址https://AtomGit.com/lqjmac/electron-markdownify-ohos欢迎加入鸿蒙PC开发者社区共同打造开发者工具生态[鸿蒙PC开发者社区]https://harmonypc.csdn.net/这次迁移的重点有三个让老 Electron 项目适配当前电脑上的 Electron 运行环境。把普通 Electron 项目放进 OpenHarmony Electron HAP 工程结构里。解决鸿蒙模拟器上窗口能打开但内容区域白屏的问题。一、项目迁移前的状态原项目是一个典型的早期 Electron Markdown 编辑器主入口是根目录下的main.js页面入口是index.html编辑器和预览逻辑主要在app/scripts/目录中。它没有npm run dev脚本普通桌面端启动方式是cd/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-masternpmstartpackage.json中现在保留了桌面端启动命令{scripts:{start:electron main.js}}刚开始在新版本 Electron 环境里跑时页面虽然能打开但按钮点击没有反应控制台能看到类似错误Cannot read properties of undefined (reading app) Cannot read properties of undefined (reading setOption) Cannot read properties of undefined (reading operation)这类问题的根因通常不是 Markdown 编辑器业务本身坏了而是旧项目依赖的 Electron API 在新版本里发生了变化。例如旧项目直接使用remote.app、remote.dialog、主进程Menu、托盘、桌面快捷键等能力而这些能力在新版 Electron 或 OpenHarmony Electron 运行时里并不总是可用。所以这次迁移采用的原则是业务层尽量不动只补运行时适配层。也就是说Markdown 文本编辑、预览、格式化按钮、同步滚动、主题切换这些原本属于业务交互的逻辑不重写只在它们调用 Electron 能力的位置做兼容保护。二、迁移后的目录结构迁移完成后项目仍然保留普通 Electron 项目根目录同时新增一个完整的鸿蒙 HAP 工程目录ohos_hap/。核心结构如下electron-markdownify-master/ ├── app/ # Markdownify 前端业务资源 ├── index.html # Electron 渲染进程入口 ├── main.js # Electron 主进程入口 ├── runtime.js # 新增运行时识别和安全调用工具 ├── config.js # 配置读写兼容层 ├── tray.js # 托盘兼容处理 ├── scripts/ │ ├── build-ohos-package.js # 同步普通 Electron 应用到 HAP 资源目录 │ └── build-ohos-hap.js # 调用 Hvigor 构建 HAP ├── ohos_hap/ # OpenHarmony Electron HAP 工程 │ ├── AppScope/ │ ├── electron/ # entry 模块 │ └── web_engine/ # Electron runtime HAR 模块 └── OHOS_ADAPTATION.md # 项目内适配说明鸿蒙运行时真正加载的 Electron 应用资源位于ohos_hap/web_engine/src/main/resources/resfile/resources/app这个目录里会被同步进以下内容resources/app/ ├── main.js ├── runtime.js ├── tray.js ├── config.js ├── index.html ├── app/ └── node_modules/三、第一步让普通 Electron 项目先在当前电脑跑起来迁移鸿蒙前先要确认普通 Electron 版本能在当前电脑环境中跑通。否则很容易把桌面 Electron 兼容问题和鸿蒙运行时问题混在一起。桌面端启动命令cd/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-masternpmstart如果需要输出 Electron 运行日志可以这样启动ELECTRON_ENABLE_LOGGING1npmstart注意这个项目没有dev脚本所以执行npm run dev会得到Missing script: dev这不是项目坏了只是脚本名不同。正确启动方式就是npm start。四、第二步建立运行时适配层为了让同一套代码同时跑在桌面 Electron 和 OpenHarmony Electron 上新增了一个runtime.js用于统一判断当前运行环境并封装安全调用。核心思路如下constisOhosprocess.platformohos||process.platformopenharmony||envLooksLikeOhos||pathLooksLikeOhos;实际适配中不能只依赖process.platform。因为鸿蒙 Electron 运行时里可能返回的是openharmony也可能在某些场景下表现得像 Linux 或其它平台。因此还需要结合安装路径判断例如/data/storage/el1/bundle/electron/resources/resfile /resources/resfile/resources/app /bundle/electron/resources/resfile最终runtime.js会导出module.exports{isOhos,platform:process.platform,diagnostics,capabilities,safeCall,safeRequire,warn};这里的capabilities用来描述当前环境是否支持桌面端能力constcapabilities{applicationMenu:!isOhos,localShortcut:!isOhos,tray:!isOhos,shellOpenExternal:!isOhos,contextMenu:!isOhos};这样主进程和渲染进程都不需要到处写一堆平台判断只要读取runtime.capabilities就能知道某个功能是否应该启用。五、第三步适配 Electron 主进程主进程的主要改造点在main.js。1. 初始化electron/remote旧项目大量依赖remote。新版 Electron 不再推荐直接使用内置remote所以项目改成使用electron/remoteconstremoteMainruntime.safeRequire(electron/remote/main,null);if(remoteMaintypeofremoteMain.initializefunction){runtime.safeCall(remoteMain.initialize,()remoteMain.initialize());}创建窗口后再启用当前窗口的 remote 能力if(remoteMaintypeofremoteMain.enablefunction){runtime.safeCall(remoteMain.enable,()remoteMain.enable(mainWindow.webContents));}这一步解决的是桌面 Electron 中remote.app、remote.dialog不存在导致的兼容问题。2. 对鸿蒙关闭桌面端能力鸿蒙运行时没有传统桌面系统菜单栏、托盘、桌面级本地快捷键等能力所以这些功能不能硬调用。适配后的主进程会根据能力判断启用if(runtime.capabilities.tray){tray.create(mainWindow);}关闭窗口时也要区分桌面端和鸿蒙端mainWindow.on(close,event{if(isQuitting||runtime.isOhos){return;}event.preventDefault();if(process.platformdarwin){app.hide();}else{mainWindow.hide();}});桌面端仍然保留“关闭窗口后隐藏到托盘/后台”的体验鸿蒙端则正常退出。3. 加主进程和渲染进程日志桥接白屏问题排查时最怕只看到窗口而看不到页面日志。所以在main.js中把渲染进程日志转发到主进程mainWindow.webContents.on(console-message,(_event,level,message,line,sourceId){console.log([markdownify-renderer:${level}]${message}(${sourceId}:${line}));});同时监听加载失败和渲染进程退出mainWindow.webContents.on(did-fail-load,(_event,errorCode,errorDescription,validatedURL){runtime.warn(mainWindow.did-fail-load,${errorCode}${errorDescription}${validatedURL||});});mainWindow.webContents.on(render-process-gone,(_event,details){runtime.warn(mainWindow.render-process-gone,JSON.stringify(details));});这样在鸿蒙hilog中就可以看到类似日志[markdownify-runtime] platformopenharmony isOhostrue ... [markdownify-renderer:1] [markdownify-renderer] index.html loaded ...六、第四步适配渲染进程中的 Electron API渲染进程主要涉及app/scripts/app.js、app/scripts/ipc_renderer.js、config.js。1. 配置路径改为 remote 优先、IPC 兜底配置模块config.js使用conf保存用户配置。桌面端可以通过electron/remote.app.getPath(userData)获取配置目录但鸿蒙运行时中这个能力可能不可用所以增加 IPC 兜底constgetUserDataPath(){try{const{app}require(electron/remote);if(apptypeofapp.getPathfunction){returnapp.getPath(userData);}}catch(_){}try{const{ipcRenderer}require(electron);returnipcRenderer.sendSync(markdownify:get-path,userData);}catch(_){return;}};主进程中对应提供ipcMain.on(markdownify:get-path,(event,name){event.returnValueruntime.safeCall(app.getPath(${name}),()app.getPath(name||userData),);});2. 文件对话框改为 remote 优先、IPC 兜底打开、保存、导出 PDF 都依赖系统文件对话框。适配后渲染进程不再直接假设dialog一定存在而是封装成varshowSaveDialogSyncoptions{if(dialogtypeofdialog.showSaveDialogSyncfunction){try{returndialog.showSaveDialogSync(options||{});}catch(error){console.warn([markdownify-runtime] remote save dialog failed:,error.message);}}try{returnipc.sendSync(markdownify:show-save-dialog-sync,options||{});}catch(error){console.warn([markdownify-runtime] ipc save dialog failed:,error.message);returnundefined;}};主进程提供同步 IPCipcMain.on(markdownify:show-save-dialog-sync,(event,options){showDialogSync(event,showSaveDialogSync,options);});这样桌面端能继续走原来的对话框能力鸿蒙端即使部分 API 不可用也不会因为一个未定义对象导致整个页面脚本崩溃。3. 鸿蒙端接管快捷键桌面端可以使用electron-localshortcut注册快捷键但鸿蒙端没有这个桌面能力。因此渲染进程里为鸿蒙运行时增加了键盘事件兜底document.addEventListener(keydown,event{varcommandevent.ctrlKey||event.metaKey;if(!command){return;}varkey(event.key||).toLowerCase();if(keys){saveFile();event.preventDefault();event.stopPropagation();}});实际代码里覆盖了常用功能CtrlN新建CtrlO打开CtrlS保存CtrlShiftS另存为CtrlB加粗CtrlI斜体CtrlF查找CtrlShiftF替换这部分属于运行时交互适配不改变 Markdown 编辑器的业务逻辑。七、第五步把普通 Electron 应用同步到鸿蒙 HAP 资源目录鸿蒙 Electron HAP 最终不是直接读取项目根目录而是读取 HAP 包内的资源目录web_engine/src/main/resources/resfile/resources/app所以新增了同步脚本scripts/build-ohos-package.js它会把普通 Electron 运行所需文件复制到 HAP 资源目录。同步内容包括[main.js,runtime.js,tray.js,config.js,index.html].forEach(filecopy(file));copy(app);writeRuntimePackageJson();copyRuntimeNodeModules();这里没有直接把整个项目粗暴复制进去而是只复制运行期必要文件并根据package-lock.json复制生产依赖。这样能减少 HAP 体积也能避免把开发脚本、缓存、无关文件带进包里。同步命令cd/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-masternpmrun ohos:sync执行后会输出OpenHarmony app resources written to: /Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-master/ohos_hap/web_engine/src/main/resources/resfile/resources/app八、第六步构建 OpenHarmony HAP迁移后的package.json增加了三个鸿蒙相关脚本{scripts:{build:ohos:node scripts/build-ohos-package.js,ohos:sync:OHOS_MARKDOWNIFY_OUTohos_hap/web_engine/src/main/resources/resfile/resources/app npm run build:ohos,ohos:build:node scripts/build-ohos-hap.js}}其中build:ohos只生成 OpenHarmony Electron 运行资源。ohos:sync把资源同步到 HAP 工程内部。ohos:build先同步资源再调用 Hvigor 构建 HAP。命令行构建cd/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-masternpmrun ohos:build构建成功后签名包位置是/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-master/ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap也可以直接用 DevEco Studio 打开/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-master/ohos_hap然后选择electronentry 模块运行。当前bundleName暂时保持为com.huawei.ohos_electron这样可以复用当前电脑已经存在的调试签名配置。如果后续要换成正式包名比如com.example.markdownify需要在 DevEco Studio 的 Signing Configs 里重新生成签名再同步修改AppScope/app.json5。九、第七步安装并启动 HAP如果命令行环境中有hdc可以直接安装/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdcinstall-r\/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-master/ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap启动/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell aa start\-bcom.huawei.ohos_electron\-aEntryAbility如果需要确认设备是否在线/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc list targets正常会看到类似127.0.0.1:5555十、白屏问题定位窗口起来了但内容区域不显示迁移过程中最关键的问题是鸿蒙模拟器上窗口标题已经出现例如New document - Markdownify说明 HAP 已安装、EntryAbility 已启动、Electron 主窗口也创建成功但窗口内容区域是白屏。当时日志里反复出现StartChildProcess command --use-glegl GPU state invalid after WaitForGetOffsetInRange GPU process start times: 131 GPU process start times: 132 GPU process start times: 133这个现象说明问题不在 Markdownify 的业务脚本而是在 Chromium/Electron GPU 子进程反复重启。页面可能已经加载但渲染面没有正常画出来。1. 先确认 JS 是否真的加载为避免把渲染层问题误判成 JS 业务错误在index.html入口增加了非常轻量的日志scriptconsole.log([markdownify-renderer] index.html loaded);window.onerrorfunction(message,source,lineno,colno,error){vardetailerrorerror.stack?error.stack:message;console.error([markdownify-renderer] window.onerror,detail,source,lineno,colno);};window.addEventListener(unhandledrejection,function(event){console.error([markdownify-renderer] unhandledrejection,event.reason);});/script启动后能在hilog里看到[markdownify-renderer] index.html loaded这说明页面入口确实加载了白屏优先怀疑 GPU/XComponent 渲染链路。2. 修正鸿蒙运行时识别一开始只用process.platform ohos判断鸿蒙结果并不可靠。模拟器日志证明鸿蒙运行时中实际输出过platformopenharmony因此runtime.js增加了路径识别constpathLooksLikeOhos[/data/storage/el1/bundle/electron/resources/resfile,/resources/resfile/resources/app,/bundle/electron/resources/resfile].some(fragmentpathHints.includes(fragment));修正后可以在日志中看到[markdownify-runtime] platformopenharmony isOhostrue envLooksLikeOhosfalse pathLooksLikeOhostrue这一步非常重要。只有isOhostrue主进程里的鸿蒙兼容分支才会执行。3. 禁用鸿蒙模拟器上的 EGL/GPU 路径仅在main.js里调用 Electron 的命令行参数还不够因为鸿蒙壳工程里还有更底层的默认启动参数。关键文件是ohos_hap/web_engine/src/main/ets/common/CommandLineAdapter.ets原始默认参数中写死了--use-glegl,模拟器白屏时日志里也一直能看到 GPU 子进程使用--use-glegl因此需要把它改为--use-gldisabled,同时增加禁用 GPU/硬件加速相关参数--disable-gpu,--disable-gpu-compositing,--disable-gpu-rasterization,--disable-accelerated-2d-canvas,--disable-accelerated-video-decode,--disable-zero-copy,--disable-gpu-watchdog,--disable-featuresEnableDrDc,SpareRendererForSitePerProcess,Vulkan,UseSkiaRenderer,CanvasOopRasterization,主进程中也同步增加if(runtime.isOhos){app.disableHardwareAcceleration();app.commandLine.appendSwitch(use-gl,disabled);app.commandLine.appendSwitch(disable-features,EnableDrDc,SpareRendererForSitePerProcess,Vulkan,UseSkiaRenderer,CanvasOopRasterization);}重新构建、安装并启动后日志从大量--use-glegl和 GPU 重启变成StartChildProcess command --use-glangle StartChildProcess command --use-gldisabled并且高频的GPU state invalid after WaitForGetOffsetInRange GPU process start times: 100不再继续刷屏页面正常显示。十一、为什么不能直接改业务层这次迁移中特别要避免一个误区看到按钮没反应、页面白屏就直接去改 Markdown 编辑器业务代码。实际上这个项目的核心业务包括Markdown 输入Markdown 转 HTML 预览CodeMirror 编辑器marked/showdown/katex/highlightjs 渲染工具栏格式化操作文件打开、保存、导出这些逻辑本身在桌面 Electron 中是可运行的。真正需要改的是业务代码和 Electron 运行时之间的连接层remote不稳定就改成electron/remote加 IPC 兜底。文件对话框不稳定就让主进程代调。桌面菜单/托盘/快捷键不适合鸿蒙就按运行时能力启用或跳过。GPU/EGL 在模拟器上白屏就调整鸿蒙壳层启动参数。也就是说迁移策略是“运行时适配优先业务逻辑最小侵入”。这样后续如果要升级 Markdownify 功能桌面端和鸿蒙端仍然可以共享同一套业务代码。十二、常用命令汇总桌面 Electron 运行cd/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-masternpmstart桌面 Electron 带日志运行ELECTRON_ENABLE_LOGGING1npmstart同步 Electron 应用到 HAP 资源目录npmrun ohos:sync构建 HAPnpmrun ohos:build安装 HAP/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdcinstall-r\/Users/luqingjiedemac/AtomGit_obj/new/electron-markdownify-master/ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap启动应用/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell aa start\-bcom.huawei.ohos_electron\-aEntryAbility查看运行时诊断日志/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell hilog-x-emarkdownify查看 GPU 参数日志/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell hilog-x-euse-gl查看 GPU 错误/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell hilog-x-eGPU state十三、迁移检查清单迁移类似 Electron 项目时可以按下面的顺序检查普通 Electron 版本是否能用npm start正常启动。是否还在使用旧的内置remote。渲染进程里是否直接调用了主进程模块例如app、dialog、Menu。是否依赖系统托盘、桌面菜单栏、本地全局快捷键。是否有nodeIntegration、contextIsolation、sandbox等窗口配置差异。HAP 资源目录是否包含main.js、index.html、业务静态资源和运行期node_modules。ohos_hap/web_engine/src/main/resources/resfile/resources/app是否是最新同步结果。CommandLineAdapter.ets是否还保留--use-glegl。hilog里是否能看到isOhostrue。hilog里是否还能持续刷GPU state invalid。十四、总结这次迁移的关键不是把 Markdownify 改成一个全新的鸿蒙应用而是把一个已有的 Electron 桌面项目包进 OpenHarmony Electron 运行时并补齐两类兼容层。第一类是 Electron API 兼容层。旧项目依赖的remote、桌面菜单、托盘、本地快捷键、文件对话框在新版 Electron 和鸿蒙 Electron 中都需要安全调用和 IPC 兜底。第二类是鸿蒙壳层启动参数兼容。模拟器白屏并不是简单的页面 JS 错误而是 GPU 子进程在--use-glegl路径下反复异常。真正解决问题的是同时修改 JS 主进程参数和CommandLineAdapter.ets的默认启动参数让鸿蒙模拟器走稳定的非 EGL 路径。最终项目达到的状态是macOS 桌面端可以继续npm start。鸿蒙端可以npm run ohos:sync同步资源。鸿蒙端可以npm run ohos:build构建 signed HAP。HAP 安装到鸿蒙模拟器后可以正常显示 Markdownify 页面。业务编辑逻辑保持基本不变主要改动集中在运行时兼容和打包工程。urces/resfile/resources/app 是否是最新同步结果。CommandLineAdapter.ets是否还保留--use-glegl。hilog里是否能看到isOhostrue。hilog里是否还能持续刷GPU state invalid。十四、总结这次迁移的关键不是把 Markdownify 改成一个全新的鸿蒙应用而是把一个已有的 Electron 桌面项目包进 OpenHarmony Electron 运行时并补齐两类兼容层。第一类是 Electron API 兼容层。旧项目依赖的remote、桌面菜单、托盘、本地快捷键、文件对话框在新版 Electron 和鸿蒙 Electron 中都需要安全调用和 IPC 兜底。第二类是鸿蒙壳层启动参数兼容。模拟器白屏并不是简单的页面 JS 错误而是 GPU 子进程在--use-glegl路径下反复异常。真正解决问题的是同时修改 JS 主进程参数和CommandLineAdapter.ets的默认启动参数让鸿蒙模拟器走稳定的非 EGL 路径。最终项目达到的状态是macOS 桌面端可以继续npm start。鸿蒙端可以npm run ohos:sync同步资源。鸿蒙端可以npm run ohos:build构建 signed HAP。HAP 安装到鸿蒙模拟器后可以正常显示 Markdownify 页面。业务编辑逻辑保持基本不变主要改动集中在运行时兼容和打包工程。