鸿蒙PC:从一个普通 Electron 项目到鸿蒙可运行项目:vmd-master 适配实战全记录
这篇文章记录了将Electron项目vmd-master迁移到鸿蒙系统的完整过程。作者从项目结构评估开始,通过六个关键步骤完成迁移:先构建鸿蒙运行环境,打通构建流程;确保基础窗口和默认页显示正常;处理鸿蒙文件选择器的返回值差异;解决路径转换和文件读取问题;最终攻克主进程读取成功但页面白屏的难题。文章特别强调了分层验证的重要性,指出鸿蒙适配中常见的路径规范化、权限持久化、进程通信等痛点,并提供了具体
前言
欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区 :https://harmonypc.csdn.net/
这篇文章记录的是一次比较完整的迁移过程:把一个原本运行在桌面端的 Electron 项目 vmd-master,一步一步改造成可以在 HarmonyOS / OpenHarmony 环境中安装、启动、打开 Markdown 文件、并正常显示内容的应用。
该适配的鸿蒙项目已经开源到:https://atomgit.com/lqjmac/vmd-ohos
这次适配不是“跑个 demo 就结束”的那种轻量改造,而是真正碰到了文件选择、路径转换、权限持久化、主进程和渲染进程通信、渲染器白屏这些实际问题。最后虽然跑通了,但过程里踩过的坑,基本都值得单独写下来。
如果你手里也有一个 Electron 项目,想迁到鸿蒙环境里跑起来,这篇文章可以当一份偏工程实践的参考。
一、项目背景和目标
vmd-master 本身是一个很典型的 Electron 小工具:打开 Markdown 文件,然后用窗口直接预览内容。
它在桌面端的运行逻辑其实很清楚:
- 主进程启动窗口
- 通过菜单或文件选择框拿到目标文件
- 主进程读文件
- 渲染进程接收内容并渲染到页面
看起来不复杂,但一旦把这套逻辑放到鸿蒙环境里,问题会立刻多起来:
- 文件选择器的返回值结构不一定和 Electron 标准实现完全一致
- 用户选择的文件路径可能不是桌面系统常见的本地绝对路径
- 文件访问权限可能不是一次性拿到后就永久有效
- 渲染器侧依赖的 Electron API、
remote能力、第三方库行为,在鸿蒙环境里不一定稳定 - 就算主进程读到文件了,也不代表页面一定能显示出来
这次迁移的目标不是“勉强能打开”,而是至少达到下面这几个要求:
- 能成功打包为鸿蒙应用
- 能启动并显示基础窗口
- 能通过鸿蒙文件选择器选中
.md文件 - 能正确读取文件内容
- 能把内容真正显示到界面上,而不是只剩一个空白壳子
二、先判断原项目的结构是否适合迁移
在动手之前,第一件事不是改代码,而是判断这个项目到底是不是“有机会改成鸿蒙版”的。
vmd-master 的优点是:
- 主业务逻辑比较集中
- UI 很简单,没有复杂的多窗口状态机
- Markdown 渲染链条相对独立
- 项目规模不大,适合快速迭代
这类项目特别适合作为 Electron 到鸿蒙适配的练手对象。因为它足够真实,能覆盖主进程、渲染器、文件系统、打包流程这些关键点;但又不至于像大型编辑器那样,一改就是成百上千个点一起联动。
如果你拿到的是一个更大的 Electron 项目,我建议先做这一步预判:
- 主进程是否过度依赖桌面原生能力
- 渲染器是否强依赖
@electron/remote - 文件系统访问是否集中在少数入口
- 是否已经有较清晰的主进程 / 渲染器分层
vmd-master 基本满足这些条件,所以值得继续做。
三、整体迁移思路:不要一开始就试图“一次适配完”
这次迁移如果总结成一句话,就是:
先让它能活,再让它能用,最后再让它像原版。
整个过程大概可以拆成四层:
- 先让项目能被鸿蒙壳启动起来
- 再解决文件选择和文件读取
- 再解决 Markdown 内容无法显示的问题
- 最后处理稳定性和工程收尾
如果一开始就想把“桌面版体验完整复刻到鸿蒙版”,基本只会把自己绕进去。更有效的做法是逐层验证:
- 窗口出来了没有
- 默认页出来了没有
- 选文件动作有回调没有
- 文件内容读出来了没有
- 内容有没有真正进入渲染器
- 页面到底是没收到数据,还是收到了但没渲染
这个分层思路,在后面定位“空白壳子”问题时特别关键。
四、第一步:把 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:ohosnpm run ohos:syncnpm 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(...)读时并不一定成功
所以这里需要做两件事:
- 路径规范化
- 读文件前后的日志确认
路径规范化这一步很关键。因为如果不先把 URI 转成真正可读的本地路径,后面 fs 相关调用的失败会很隐蔽,你看到的可能只是界面没内容,而不是明确的异常。
这次迁移里,主进程增加了一层本地路径转换逻辑,把鸿蒙文件 URI 尽量转换成后续统一可用的 resolvedFilePath。
之后再围绕这条链路加日志:
sendMarkdown:startsendMarkdown:read-success
这样至少能确认:文件到底有没有被真正读到。

九、第六步:最难的一关,其实是“主进程读到了,但页面还是白的”
这个阶段是整次适配里最绕的一段,也是最值得写进文章的部分。
当时已经确认了这些事实:
- 文件选择成功
- 选中的 Markdown 路径也拿到了
- 主进程确实能读到文件内容
- 文件标题甚至都更新到了窗口标题栏上
按道理讲,这时候页面应该至少出现文字了。
但实际现象是:
- 窗口打开了
- 标题栏正常
- 菜单栏正常
- 内容区是一整块白色
这一步如果只凭感觉猜,很容易误判成:
- CSS 把内容盖住了
- Markdown 没渲染成功
- 内容发送到了错误窗口
但把日志串起来之后,问题慢慢变清楚了:
- 主进程有日志
- 渲染器几乎没有有效回声
default.html也能被发出去,但欢迎页依然不显示
这就说明真正的问题不是 Markdown 本身,而是:
鸿蒙上的渲染器入口没有稳定地接住主进程发来的内容。
也就是说,这时候不能再盯着“Markdown 渲染库”查了,而要回到更前一层去看:
- 渲染器脚本是不是根本没完整启动
- IPC 消息是不是发早了
- 渲染器有没有在鸿蒙环境里因为某个依赖提前崩掉
这类问题如果不强行分层,很容易越修越乱。

十、第七步:不要死磕旧渲染器,直接拆一个鸿蒙专用最小入口
到了白壳阶段,再继续在原有桌面版渲染器上硬修,成本会越来越高。
原因也很简单:桌面版渲染器通常会依赖一串桌面环境里理所当然、但在鸿蒙环境里未必稳的东西,比如:
@electron/remote- 搜索插件
- 历史状态
- 剪贴板
- 右键菜单
- 复杂的页面初始化逻辑
这些能力在桌面端很正常,但在鸿蒙环境里,它们会增加渲染器启动失败的概率。
所以这次真正把问题打通的关键思路不是继续补桌面逻辑,而是:
单独给 Harmony 准备一个最小渲染入口。
这个最小入口只做三件事:
- 告诉主进程“我已经 ready 了”
- 接收
md消息 - 把收到的 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 resolvedopenFileInReaderchangeFilesendMarkdown:startsendMarkdown:read-successsendMarkdown:emitrenderer-ready
后面的定位会困难很多。
十四、如果你也要迁移一个 Electron 项目,我建议按这个顺序来
这是我认为比较稳的一条顺序:
- 先让鸿蒙壳工程能打包出 HAP
- 先验证窗口能否打开
- 先验证默认欢迎页能否显示
- 再接文件选择器
- 再做路径归一化和权限持久化
- 再验证主进程是否能稳定读文件
- 再验证内容是否真正进入渲染器
- 如果渲染器不稳,直接拆一个平台专用最小入口
- 最后再考虑把桌面版增强能力补回去
这个顺序的好处是,每一步都能单独成立。你不会在“十个点同时可能出错”的情况下瞎猜。
十五、结论
这次把 vmd-master 适配到鸿蒙,真正让我印象最深的不是代码改了多少,而是平台迁移这件事本身的节奏感。
很多时候你以为自己在解决一个“渲染问题”,最后发现其实是 IPC 时机问题;你以为自己在查“文件打不开”,最后发现只是路径格式根本没统一。
所以这类工作最怕一开始就闷头改,最有用的反而是:
- 先分层
- 再打点
- 再逐步收口
当默认页能出来、文件能选、内容能读、渲染器能接住消息以后,后面的事情反而就顺了。
如果把这次适配当成一个经验总结,我会觉得最重要的一条是:
新平台上的迁移,不是复制原有实现,而是重新建立一条在新环境里真正可靠的运行链路。
更多推荐



所有评论(0)