前言

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

这篇文章记录的是一次比较完整的迁移过程:把一个原本运行在桌面端的 Electron 项目 vmd-master,一步一步改造成可以在 HarmonyOS / OpenHarmony 环境中安装、启动、打开 Markdown 文件、并正常显示内容的应用。

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

这次适配不是“跑个 demo 就结束”的那种轻量改造,而是真正碰到了文件选择、路径转换、权限持久化、主进程和渲染进程通信、渲染器白屏这些实际问题。最后虽然跑通了,但过程里踩过的坑,基本都值得单独写下来。

如果你手里也有一个 Electron 项目,想迁到鸿蒙环境里跑起来,这篇文章可以当一份偏工程实践的参考。


一、项目背景和目标

vmd-master 本身是一个很典型的 Electron 小工具:打开 Markdown 文件,然后用窗口直接预览内容。

它在桌面端的运行逻辑其实很清楚:

  1. 主进程启动窗口
  2. 通过菜单或文件选择框拿到目标文件
  3. 主进程读文件
  4. 渲染进程接收内容并渲染到页面

看起来不复杂,但一旦把这套逻辑放到鸿蒙环境里,问题会立刻多起来:

  • 文件选择器的返回值结构不一定和 Electron 标准实现完全一致
  • 用户选择的文件路径可能不是桌面系统常见的本地绝对路径
  • 文件访问权限可能不是一次性拿到后就永久有效
  • 渲染器侧依赖的 Electron API、remote 能力、第三方库行为,在鸿蒙环境里不一定稳定
  • 就算主进程读到文件了,也不代表页面一定能显示出来

这次迁移的目标不是“勉强能打开”,而是至少达到下面这几个要求:

  • 能成功打包为鸿蒙应用
  • 能启动并显示基础窗口
  • 能通过鸿蒙文件选择器选中 .md 文件
  • 能正确读取文件内容
  • 能把内容真正显示到界面上,而不是只剩一个空白壳子

二、先判断原项目的结构是否适合迁移

在动手之前,第一件事不是改代码,而是判断这个项目到底是不是“有机会改成鸿蒙版”的。

vmd-master 的优点是:

  • 主业务逻辑比较集中
  • UI 很简单,没有复杂的多窗口状态机
  • Markdown 渲染链条相对独立
  • 项目规模不大,适合快速迭代

这类项目特别适合作为 Electron 到鸿蒙适配的练手对象。因为它足够真实,能覆盖主进程、渲染器、文件系统、打包流程这些关键点;但又不至于像大型编辑器那样,一改就是成百上千个点一起联动。

如果你拿到的是一个更大的 Electron 项目,我建议先做这一步预判:

  • 主进程是否过度依赖桌面原生能力
  • 渲染器是否强依赖 @electron/remote
  • 文件系统访问是否集中在少数入口
  • 是否已经有较清晰的主进程 / 渲染器分层

vmd-master 基本满足这些条件,所以值得继续做。


三、整体迁移思路:不要一开始就试图“一次适配完”

这次迁移如果总结成一句话,就是:

先让它能活,再让它能用,最后再让它像原版。

整个过程大概可以拆成四层:

  1. 先让项目能被鸿蒙壳启动起来
  2. 再解决文件选择和文件读取
  3. 再解决 Markdown 内容无法显示的问题
  4. 最后处理稳定性和工程收尾

如果一开始就想把“桌面版体验完整复刻到鸿蒙版”,基本只会把自己绕进去。更有效的做法是逐层验证:

  • 窗口出来了没有
  • 默认页出来了没有
  • 选文件动作有回调没有
  • 文件内容读出来了没有
  • 内容有没有真正进入渲染器
  • 页面到底是没收到数据,还是收到了但没渲染

这个分层思路,在后面定位“空白壳子”问题时特别关键。


四、第一步:把 Electron 项目塞进鸿蒙可运行的壳里

桌面版 Electron 项目本身不能直接拿去给鸿蒙跑,必须先有一层鸿蒙侧承载容器。

这里的核心思路不是重写应用,而是:

  • 保留原来的 Electron 应用资源
  • 在鸿蒙侧准备 ohos_hap/ 工程
  • 让鸿蒙壳把 Electron 运行环境和前端资源一起带起来

从目录组织上看,最终形成的结构大概是这样的:

vmd-master/
├── main/
├── renderer/
├── shared/
├── scripts/
├── ohos_hap/
│   ├── electron/
│   └── web_engine/
└── package.json

这里 ohos_hap/ 不是原项目自带的 Electron 目录,而是面向鸿蒙打包和运行的一层工程。

在这一步里,最重要的不是业务代码,而是先把下面几件事建立起来:

  • Electron 资源如何同步到鸿蒙工程里
  • 鸿蒙构建脚本怎么触发
  • 最终产物 .hap 放在哪

在这里插入图片描述


五、第二步:把构建流程打通

一个迁移项目,如果没有稳定的构建命令,后面每修一次 bug 都会变得很痛苦。

这次适配里,比较重要的几个脚本是:

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

它们的分工可以理解成:

  • build:ohos:把应用资源整理成鸿蒙侧能消费的形式
  • ohos:sync:把资源同步到 ohos_hap 指定目录
  • ohos:build:触发完整 HAP 构建

实际工程里,这一步的价值很大。因为你后面每改一次:

  • 主进程文件读取
  • 文件选择逻辑
  • 渲染器脚本
  • 页面模板

最后都得靠这条构建链验证结果。

如果脚本层不稳定,就会经常陷入一种很糟糕的状态:你以为自己在验证代码问题,实际上只是资源没同步进去,或者打包产物不是最新版本。

所以我建议把这一步当成正式工程的一部分,而不是临时拼一下能用就行。

在这里插入图片描述


六、第三步:先让默认页显示出来

刚进入鸿蒙适配时,一个很容易忽略的问题是:
能启动窗口,不代表页面已经正常显示。

vmd-master 默认会打开 renderer/default.html,也就是欢迎页。这个欢迎页其实是最适合用来做第一阶段验证的:

  • 它不依赖用户选文件
  • 不依赖 Markdown 渲染
  • 只要渲染器链路正常,理论上就应该显示出来

如果连默认页都出不来,后面再去查 Markdown 打不开,其实意义不大。因为你连最基础的页面渲染都还没站稳。

这一步的判断标准应该很简单:

  • 窗口能否打开
  • 标题栏是否正常
  • 欢迎页内容是否显示
  • 菜单栏是否可操作

如果这一步只剩下“一个白窗口壳子”,那就说明问题很可能不在业务数据,而在渲染器入口、前端脚本执行、或主进程消息发送时机。

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

七、第四步:处理鸿蒙文件选择器和返回值差异

窗口跑起来之后,真正进入可用阶段的第一道门槛,就是“选文件”。

在桌面版 Electron 里,dialog.showOpenDialog(...) 的返回结果大家都很熟,通常是这样的结构:

{
  canceled: false,
  filePaths: ['/path/to/file.md']
}

但在鸿蒙这边,适配层未必严格按 Electron 的标准结果返回。你可能碰到的情况包括:

  • 直接返回数组
  • 返回字符串
  • 返回 { paths: [...] }
  • 返回 { filePath: '...' }

这就意味着,主进程不能偷懒地只写一种解析方式,否则表面看上去“调用成功了”,实际上后面根本拿不到文件。

这次适配里,比较关键的一步就是把文件选择结果做了一层统一归一化,也就是不管鸿蒙侧吐回来哪种结构,最后都整理成标准的:

{
  canceled,
  filePaths
}

这样后续的打开逻辑才不会到处塞兼容判断。

在这里插入图片描述


八、第五步:路径能拿到,不代表文件能读

这一步是鸿蒙适配里特别容易让人产生错觉的地方。

你看到文件选择器成功返回了路径,很容易就觉得“行了,后面读文件应该没问题”。但实际情况是,鸿蒙环境里的文件选择结果常常不是你平时熟悉的桌面绝对路径。

常见问题包括:

  • 拿到的是 file:// 形式的 URI
  • 拿到的是特殊域名形式,比如 file://docs/...
  • 当前进程能看到这个值,但真正用 fs.readFileSync(...) 读时并不一定成功

所以这里需要做两件事:

  1. 路径规范化
  2. 读文件前后的日志确认

路径规范化这一步很关键。因为如果不先把 URI 转成真正可读的本地路径,后面 fs 相关调用的失败会很隐蔽,你看到的可能只是界面没内容,而不是明确的异常。

这次迁移里,主进程增加了一层本地路径转换逻辑,把鸿蒙文件 URI 尽量转换成后续统一可用的 resolvedFilePath

之后再围绕这条链路加日志:

  • sendMarkdown:start
  • sendMarkdown:read-success

这样至少能确认:文件到底有没有被真正读到。

在这里插入图片描述


九、第六步:最难的一关,其实是“主进程读到了,但页面还是白的”

这个阶段是整次适配里最绕的一段,也是最值得写进文章的部分。

当时已经确认了这些事实:

  • 文件选择成功
  • 选中的 Markdown 路径也拿到了
  • 主进程确实能读到文件内容
  • 文件标题甚至都更新到了窗口标题栏上

按道理讲,这时候页面应该至少出现文字了。

但实际现象是:

  • 窗口打开了
  • 标题栏正常
  • 菜单栏正常
  • 内容区是一整块白色

这一步如果只凭感觉猜,很容易误判成:

  • CSS 把内容盖住了
  • Markdown 没渲染成功
  • 内容发送到了错误窗口

但把日志串起来之后,问题慢慢变清楚了:

  • 主进程有日志
  • 渲染器几乎没有有效回声
  • default.html 也能被发出去,但欢迎页依然不显示

这就说明真正的问题不是 Markdown 本身,而是:

鸿蒙上的渲染器入口没有稳定地接住主进程发来的内容。

也就是说,这时候不能再盯着“Markdown 渲染库”查了,而要回到更前一层去看:

  • 渲染器脚本是不是根本没完整启动
  • IPC 消息是不是发早了
  • 渲染器有没有在鸿蒙环境里因为某个依赖提前崩掉

这类问题如果不强行分层,很容易越修越乱。

在这里插入图片描述


十、第七步:不要死磕旧渲染器,直接拆一个鸿蒙专用最小入口

到了白壳阶段,再继续在原有桌面版渲染器上硬修,成本会越来越高。

原因也很简单:桌面版渲染器通常会依赖一串桌面环境里理所当然、但在鸿蒙环境里未必稳的东西,比如:

  • @electron/remote
  • 搜索插件
  • 历史状态
  • 剪贴板
  • 右键菜单
  • 复杂的页面初始化逻辑

这些能力在桌面端很正常,但在鸿蒙环境里,它们会增加渲染器启动失败的概率。

所以这次真正把问题打通的关键思路不是继续补桌面逻辑,而是:

单独给 Harmony 准备一个最小渲染入口。

这个最小入口只做三件事:

  1. 告诉主进程“我已经 ready 了”
  2. 接收 md 消息
  3. 把收到的 HTML 或文本直接塞进页面

这样做的好处非常直接:

  • 渲染器启动链路变短
  • 少掉很多不必要依赖
  • 更容易判断问题是“消息没到”还是“页面没画”

这一步实际上是一种很典型的迁移策略:

不要一开始就追求功能完全一致,先做平台最小可运行版本。

等最小入口稳定之后,再逐步把桌面版能力往回补。

在这里插入图片描述


十一、第八步:给主进程和渲染器之间加握手,不要盲发消息

很多桌面端场景里,窗口 did-finish-load 之后,主进程直接 win.webContents.send(...) 就够了。

但在鸿蒙环境里,这个时机未必真的意味着渲染器脚本已经全部准备好。

所以这次后面又补了一层更稳妥的机制:

  • 渲染器启动后主动发 renderer-ready
  • 主进程在收到 ready 之前,先把待发送内容暂存
  • ready 到了以后,再正式把 md 内容发出去

这个改动不大,但很关键。因为它解决的是“消息可能发到了空气里”的问题。

对排查白屏类问题来说,这种握手机制非常有价值。它能把问题明显分成两类:

  • renderer-ready 都没有,说明渲染器入口没起来
  • renderer-ready 有了,但 onContent 没执行,说明 IPC 链路有问题
  • onContent 有了,但界面还是白的,说明是 DOM 或 CSS 层的问题

有了这层分界之后,后面的定位就不会再混成一锅。

在这里插入图片描述


十二、第九步:把 Markdown 渲染尽量前移到主进程

虽然最后真正打通白壳问题的是鸿蒙专用渲染入口,但还有一个很重要的策略也值得保留:

尽量把 Markdown 渲染前移到主进程。

原因很现实:

  • 主进程文件读取已经验证通过
  • 主进程日志更容易看
  • 鸿蒙上渲染器侧的第三方库链条越长,越不稳定
  • 如果主进程已经能直接产出 HTML,那渲染器只需要负责显示

从架构上说,这相当于把问题从:

“文件读取 + Markdown 渲染 + 页面显示”

压缩成:

“页面显示”

一旦能把复杂逻辑往主进程收,渲染器就更像一个简单展示层,适配成本会低很多。

这个思路不一定适合所有 Electron 项目,但对这种“文件读取 + 纯内容展示型”应用来说,特别有效。


十三、这次迁移里最值得记住的几个坑

1. 看到文件路径,不代表有权限读文件

路径拿到了,只能说明文件选择器那一步完成了。
真正能不能读,还得看路径格式和权限是否被持久化。

2. 主进程读到内容,不代表渲染器一定能显示

这是本次最核心的误区。
如果没有日志分层,很容易一直在错误方向上修。

3. 默认页都出不来时,不要先查 Markdown

欢迎页都白了,问题一定在更基础的地方。

4. 在新平台上,先做最小入口比强拉原逻辑更靠谱

很多桌面端“理所当然可用”的依赖,到新平台上都可能成为负担。

5. 迁移时最宝贵的不是代码量,而是日志设计

这次如果没有这些日志:

  • showOpenDialog resolved
  • openFileInReader
  • changeFile
  • sendMarkdown:start
  • sendMarkdown:read-success
  • sendMarkdown:emit
  • renderer-ready

后面的定位会困难很多。


十四、如果你也要迁移一个 Electron 项目,我建议按这个顺序来

这是我认为比较稳的一条顺序:

  1. 先让鸿蒙壳工程能打包出 HAP
  2. 先验证窗口能否打开
  3. 先验证默认欢迎页能否显示
  4. 再接文件选择器
  5. 再做路径归一化和权限持久化
  6. 再验证主进程是否能稳定读文件
  7. 再验证内容是否真正进入渲染器
  8. 如果渲染器不稳,直接拆一个平台专用最小入口
  9. 最后再考虑把桌面版增强能力补回去

这个顺序的好处是,每一步都能单独成立。你不会在“十个点同时可能出错”的情况下瞎猜。


十五、结论

这次把 vmd-master 适配到鸿蒙,真正让我印象最深的不是代码改了多少,而是平台迁移这件事本身的节奏感。

很多时候你以为自己在解决一个“渲染问题”,最后发现其实是 IPC 时机问题;你以为自己在查“文件打不开”,最后发现只是路径格式根本没统一。

所以这类工作最怕一开始就闷头改,最有用的反而是:

  • 先分层
  • 再打点
  • 再逐步收口

当默认页能出来、文件能选、内容能读、渲染器能接住消息以后,后面的事情反而就顺了。

如果把这次适配当成一个经验总结,我会觉得最重要的一条是:

新平台上的迁移,不是复制原有实现,而是重新建立一条在新环境里真正可靠的运行链路。

Logo

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

更多推荐