前言

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区 :https://harmonypc.csdn.net/

这篇文章记录的是一次比较完整的 Electron 项目鸿蒙适配过程。

项目本体是 Pomotroid,一个界面不复杂、功能也很聚焦的番茄钟工具。桌面端体验其实很干净:启动应用、开始计时、切换工作和休息轮次、打开设置页、查看统计页,整个交互链路都很顺。

但把这样一个 Electron 项目迁到 OpenHarmony / HarmonyOS 环境里之后,问题并不会因为“它只是个番茄钟”就变少。真正动手后会发现,桌面端默认成立的很多前提,到鸿蒙里都要重新确认一遍:

  • 主进程入口能不能正常拉起来
  • 前端资源是否能被正确打包和加载
  • preload 桥接脚本能不能执行
  • 多窗口逻辑在鸿蒙里是不是仍然成立
  • GPU 进程是否稳定
  • 页面按钮点下去到底有没有事件,还是事件到了但目标窗口根本不存在

这次适配不是“把窗口点亮”就结束,而是一路从:

能编译 -> 能安装 -> 能启动 -> 能加载页面 -> 能初始化计时器 -> 能打开设置和统计 -> 基本可用

这样一步一步走过来的。

如果你手里也有一个 Electron 小工具,正准备迁到鸿蒙环境,这篇文章应该能给你一个比较真实的参考。

该项目适配的鸿蒙版本已经开源到:https://atomgit.com/lqjmac/pomotroid-ohos


一、这次适配的目标,不只是“跑起来”

一开始给自己定的目标其实很明确,不做那种“勉强能截图交差”的适配。

至少要做到下面几件事:

  1. 能稳定打包成鸿蒙应用
  2. 安装后可以正常启动
  3. 主页面能完成初始化,而不是黑屏或白屏
  4. 计时器逻辑能工作
  5. 设置页和统计页可以打开
  6. 页面交互不是摆设,用户点了按钮要有真实反馈

从结果看,这些目标最后都达到了,但中间踩过的坑也挺典型,尤其适合拿出来单独讲讲。

在这里插入图片描述


二、项目起点:能构建,但真正运行时问题一串

刚接手这个项目时,代码层面并不是从零开始。项目里已经有一套面向鸿蒙的承载工程,也有基础脚本可以把 Web 资源和 Electron 资源同步进 ohos_hap 目录。

当时比较关键的几个脚本已经具备雏形:

  • npm run build
  • npm run ohos:sync
  • npm run ohos:build

这意味着“构建通道”不是完全断的,能减少很多纯工程初始化时间。

但真正把应用装到设备上以后,问题马上就出来了。最早看到的并不是页面内容,而是一串运行日志,里面最关键的几类报错包括:

  • 页面虽然试图加载本地 index.html,但渲染器没有正常起来
  • preload 脚本报语法错误
  • window.pomotroidDesktop 为空,前端桥接能力直接失效
  • GPU 进程不断重启
  • 设置页和统计页点击后没有反应

也就是说,当时的状态不是“差一点就好了”,而是链路上有好几个断点,只不过它们是前后叠在一起的。


三、第一关:先把前端资源真正装进鸿蒙包里

Electron 项目迁鸿蒙时,最容易低估的一件事,就是资源打包路径。

桌面端很多情况下,前端页面和运行时资源默认就在 Electron 应用目录附近,路径组织也相对固定。但到了鸿蒙侧,最后真正跑起来的资源位置已经变成了打包产物内部结构的一部分,比如:

/data/storage/el1/bundle/.../resources/resfile/resources/app/...

如果前端还按桌面习惯去假设资源路径,很容易出现下面这种情况:

  • index.html 是能找到的
  • /_app/... 这些静态资源还是按根路径请求
  • 最终页面壳子打开了,JS 和 CSS 实际上没正确加载

这一步的处理重点有两个:

  1. 用构建脚本把 buildelectronstatic 和运行所需依赖统一同步到鸿蒙资源目录
  2. build/index.html 做一次改写,把原本依赖根路径的 /_app/... 改成相对路径 ./_app/...

这类修改不算复杂,但很关键。因为如果静态资源路径不对,后面看到的一切白屏、黑屏、空页面都不一定是真的业务逻辑问题。

在这里插入图片描述


四、第二关:preload 不是小问题,它直接决定前端桥接是否存在

真正把页面跑起来的过程中,最关键的一个断点其实是 preload

当时设备日志里有两句信息非常致命:

  • Unable to load preload script
  • SyntaxError: Cannot use import statement outside a module

这两句一出来,问题就很清楚了。

桌面 Electron 项目里,preload 用 ESM 写法并不奇怪,比如直接:

import { contextBridge, ipcRenderer } from 'electron';

但在这套鸿蒙 Electron 壳里,preload 运行方式和桌面环境并不完全一致。这里它期待的是一个经典 CommonJS preload,而不是 ESM 模块。

结果就是:

  1. preload 没执行成功
  2. contextBridge.exposeInMainWorld(...) 没跑
  3. window.pomotroidDesktop 为空
  4. 前端调用桥接 API 时直接报错
  5. 主页面初始化流程中断

所以这一步真正做的修复是:

  • preload.js 改成 CommonJS 版本
  • 新增 preload.cjs
  • 主窗口 webPreferences.preload 明确改为指向 preload.cjs

修完这一步以后,前端桥终于回来了,后面日志里也开始出现真正有价值的渲染器初始化信息。

在这里插入图片描述
在这里插入图片描述

五、第三关:页面终于开始初始化了,但 GPU 进程又开始掉链子

preload 修完以后,事情明显往前走了一大步。

日志里终于能看到这些信息:

  • [renderer] timer init:start
  • [renderer] timer initial state ...
  • [renderer] timer init:complete
  • [renderer] main page settings loaded
  • [renderer] main page themes loaded
  • [renderer] main page init:complete

看到这几行的时候,基本就可以确认:

  • 页面资源已经加载了
  • 前端桥可用了
  • 主页面初始化逻辑也跑完了

但这时候又出现了另一个问题:GPU 进程开始疯狂重启。

日志里会不断刷类似的内容:

  • GPU state invalid after WaitForGetOffsetInRange
  • GPU process start times: 1, 2, 3, 4...

这类问题在桌面 Electron 上不一定常见,但在鸿蒙壳里并不少见。它的危险之处在于:页面看起来可能勉强能显示,但底层渲染进程会非常不稳定,日志也会被 GPU 重启刷满。

这一步做的处理主要是两层:

  1. 在应用启动尽可能早的阶段禁用硬件加速
  2. 不要把软件回退路径一起禁掉

后来做了两项调整:

  • 在真正引导 Electron 主入口之前,就在 main.cjs 里提前执行 app.disableHardwareAcceleration()disable-gpu 相关开关
  • 去掉 disable-software-rasterizer,避免把软件渲染回退也一并堵死

这一步不是为了“完全没有 GPU 日志”,而是为了让应用先稳定地活下来。

六、第四关:主页面可用了,但“设置”和“统计”依旧点不开

这一步很有代表性,因为它看上去像是“按钮没响应”,但实际上根因完全不是点击事件本身。

当时的现象是:

  • 主页面能显示
  • 鼠标事件能进来
  • 光标状态会变化
  • 但点击“设置”和“统计”以后,界面没有切换

继续看日志,关键线索就出来了:

  • AppWindowAdapter#showWindow(4)
  • AbilityManager --> getProxy failed, proxyId: browser4 not found
  • AppWindowAdapter#showWindow(3)
  • AbilityManager --> getProxy failed, proxyId: browser3 not found

看到这里就知道了,问题不在“点没点到”,而在“点到了之后走错了路线”。

原项目的顶部按钮逻辑,本来沿用的是桌面版多窗口方案:

  • 设置页单独开一个窗口
  • 统计页单独开一个窗口

这在桌面 Electron 里很正常,但鸿蒙这边当前实际只有主窗口 browser1,并没有对应的 browser3browser4 代理存在。

所以按钮点下去以后,不是没触发,而是在请求打开一个鸿蒙壳根本不存在的子窗口。


七、这一步的真正修法:OHOS 下不要硬搬桌面多窗口,改成单窗口路由

既然鸿蒙侧没有稳定可用的多窗口代理,那就不要硬顶着桌面交互模型往前撞了。

这一步最后采取的策略很直接:

  • 桌面平台继续保留原来的多窗口行为
  • OpenHarmony / HarmonyOS 下改成单窗口路由切换

也就是说:

  • 点击“设置”时,OHOS 下不再 window_create('settings')
  • 而是直接在主窗口切到 /settings
  • 点击“统计”时同理,切到 /stats
  • 设置页和统计页自己的关闭按钮,也不再尝试 close() 一个独立窗口,而是直接返回主页 /

这样做的好处很明显:

  1. 不需要依赖额外的 browser3/browser4
  2. 主窗口内完成页面切换,行为更可控
  3. 对鸿蒙现阶段运行环境更友好

这一步做完之后,设置页和统计页终于真正可用了。

在这里插入图片描述
在这里插入图片描述

八、构建流程也顺手补齐了,不然每修一次都很难验证

适配这类项目时,如果没有稳定的构建脚本,后面的调试会非常痛苦。

这次过程中,最终比较稳定的一套命令是:

npm run build
npm run ohos:sync
npm run ohos:build

它们各自承担的职责也比较明确:

  • build:生成前端静态资源
  • ohos:sync:把前端和 Electron 运行资源同步进鸿蒙工程
  • ohos:build:触发完整 HAP 构建

中间还有一个实际踩到的工程问题,是 hvigor 守护进程的锁文件。

当时如果直接在受限环境里跑 HAP 构建,会因为 ~/.hvigor/... 下的 daemon lock 导致构建失败。后面把这个问题理顺以后,assembleHap 才真正稳定下来。

这类问题不算业务逻辑,但如果构建通道不稳定,前面每做一次修复都很难确认是不是已经真正进包了。

在这里插入图片描述


九、这次适配里最值得记住的几个判断节点

回头看,这个项目虽然不算大型应用,但它把 Electron 迁鸿蒙时最常见的几个坑几乎都碰了一遍。

如果把整个过程压缩成几个最重要的判断节点,我觉得有这几条:

1. 白屏不一定是前端逻辑错了,先看资源路径

很多时候页面出不来,不是框架问题,而是静态资源根路径假设错了。

2. 前端报桥接为空时,先查 preload

只要 window.xxx 这种桥接对象没出现,就不要急着查业务页面,先确认 preload 是否真的执行了。

3. 日志里出现初始化完成,不代表整体就没问题了

Pomotroid 这次就是典型例子:前端初始化完成后,GPU 进程仍然在后台疯狂崩。

4. “按钮没反应”不等于“点击事件没触发”

设置和统计页这个问题,表面像前端按钮失灵,实际上是后面的窗口目标根本不存在。

5. 不要强行把桌面交互模型一比一搬到鸿蒙

桌面上多窗口很自然,但鸿蒙适配里有时候更务实的方案是:先改成单窗口路由,把功能做通。


十、最终结果:Pomotroid 已经具备鸿蒙侧基本可用性

经过这一轮处理之后,Pomotroid 在鸿蒙上的状态已经和最开始完全不是一个阶段了。

目前已经完成的部分包括:

  • 前端资源可正确打包并加载
  • preload 桥恢复正常
  • 主页面初始化完成
  • 计时器状态可读取和展示
  • 设置页可进入
  • 统计页可进入
  • 设置页和统计页在 OHOS 下采用单窗口路由切换
  • HAP 可以稳定构建输出

十一、最后一点感受

这次做下来,我对 Electron 迁鸿蒙这件事有一个更明确的感受:

真正难的地方,往往不是改 UI,也不是把某个命令跑通,而是持续判断“当前这个问题到底断在链路哪一层”。

你如果把问题层次看错了,调试会非常绕。

比如:

  • 明明是 preload 没起来,却一直在前端页面里找状态问题
  • 明明是 GPU 进程崩,却以为是主题切换导致界面卡死
  • 明明是多窗口代理不存在,却误判成按钮点击丢失

反过来说,只要每一步都能把“断点到底在哪”这件事判断清楚,哪怕项目不是特别小,适配也还是能稳步往前推。

Pomotroid 这次就是一个挺典型的例子。

它不是一上来就顺的,但也不是那种完全没法做的项目。真正把链路一段一段接上以后,最后呈现出来的结果其实很扎实。

Logo

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

更多推荐