Electron for 鸿蒙PC - MarkRight应用完整适配实践
本文介绍了将MarkRight(一款GitHub风格Markdown编辑器)适配到HarmonyOS PC平台的过程。通过Electron for HarmonyOS框架,重点解决了四个技术难点:1)自定义ClojureScript IPC通信系统的适配,实现了消息序列化与进程间通信;2)修复了图片路径处理逻辑,支持相对路径、绝对路径和HTTP图片;3)Shell模块访问权限配置;4)Browse
📋 项目概述
本文档详细介绍了如何将 MarkRight(一个极简主义的 GitHub Flavored Markdown 编辑器)成功适配到 HarmonyOS PC 平台。基于 Electron for HarmonyOS 框架,通过深度适配和优化,实现了在鸿蒙 PC 上的完美运行。
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
📸 效果展示
以下是 MarkRight 在 HarmonyOS PC 平台上的运行效果:
演示示例 1

MarkRight 应用主界面在 HarmonyOS PC 上的显示效果
演示示例 2

代码高亮、实时预览和 Markdown 渲染效果
从效果图可以看出,MarkRight 应用已成功适配到 HarmonyOS PC 平台,界面显示正常,功能完整可用。
项目背景
MarkRight 是一个使用 ClojureScript 编写的 Electron 应用,具有以下特点:
- 使用 ClojureScript 编译生成 JavaScript
- 自定义 IPC 通信系统(使用 ClojureScript 的
pr-str和read-string) - 使用旧版 Electron API(0.33.6)
- 依赖多个 Node.js native 模块
适配挑战
- 自定义 IPC 通信系统:MarkRight 使用 ClojureScript 格式的消息序列化
- 图片路径处理:需要正确处理相对路径、绝对路径和 HTTP 图片
- Shell 模块访问:渲染进程中需要访问
shell.openExternal() - BrowserWindow 配置:需要确保所有窗口都正确注入 preload 脚本
🎯 核心功能实现
1. 自定义 IPC 通信系统
问题分析
MarkRight 使用 ClojureScript 的 pr-str 和 read-string 进行消息序列化,通过 electron.ipc 通道进行通信。消息格式如下:
{:electron.ipc/op :cast, :electron.ipc/action :load-file}
{:electron.ipc/op :call, :electron.ipc/action :get-current-content}
解决方案
1.1 主进程 IPC 处理器
在 main.js 中添加 electron.ipc 通道监听器:
ipcMain.on('electron.ipc', (event, message) => {
console.log('[HarmonyOS IPC] Received MarkRight IPC message');
try {
let electronIPC = null;
// 从全局作用域获取 electron.ipc(由 app.js.original 导出)
if (typeof global.electron !== 'undefined' && global.electron && global.electron.ipc) {
electronIPC = global.electron.ipc;
}
if (electronIPC && typeof electronIPC.process_msg === 'function') {
// 创建 IPC 回复对象
const ipcForReply = {
send: function(channel, msg) {
if (event.sender && !event.sender.isDestroyed()) {
event.sender.send(channel, msg);
}
}
};
// 设置 IPC 目标
if (electronIPC._STAR_ipc_target_STAR_) {
electronIPC.cljs$core$vreset_BANG_(
electronIPC._STAR_ipc_target_STAR_,
ipcForReply
);
}
// 处理消息
electronIPC.process_msg(event, message);
} else {
// 如果 electron.ipc 尚未加载,将消息加入队列
if (!global.__markrightIPCMessageQueue) {
global.__markrightIPCMessageQueue = [];
}
global.__markrightIPCMessageQueue.push({ event, message });
}
} catch (error) {
console.error('[HarmonyOS IPC] Error processing message:', error);
}
});
1.2 导出 electron 对象
在 app.js.original 末尾添加:
// 导出 electron 对象到全局作用域,供 IPC 处理器使用
global.electron = electron;
1.3 处理队列消息
在 MarkRight 加载完成后处理队列中的消息:
// 处理队列中的 IPC 消息
if (global.__markrightIPCMessageQueue && global.__markrightIPCMessageQueue.length > 0) {
const queue = [...global.__markrightIPCMessageQueue];
global.__markrightIPCMessageQueue = [];
queue.forEach(({ event, message }) => {
ipcMain.emit('electron.ipc', event, message);
});
}
1.4 设置窗口 IPC 目标
在创建 BrowserWindow 时设置 IPC 目标:
// 在 HarmonyOSBrowserWindow 构造函数中
if (global.__createMarkRightIPCWrapper) {
const ipcWrapper = global.__createMarkRightIPCWrapper(win);
if (global.electron && global.electron.ipc) {
global.electron.ipc.cljs$core$vreset_BANG_(
global.electron.ipc._STAR_ipc_target_STAR_,
ipcWrapper
);
}
}
2. 图片路径处理
问题分析
MarkRight 的 parse-images! 函数会将所有非 HTTP 图片路径转换为 file:// URL,但对于绝对路径图片,会错误地拼接 current-path,导致路径错误。
例如:
- 绝对路径:
/Users/name/pic.png - 错误结果:
file:///path/to/md/file//Users/name/pic.png
解决方案
2.1 创建图片路径修复补丁
创建 ui/js/image-path-fix.js:
(function() {
'use strict';
// 检测绝对路径
function isAbsolutePath(path) {
if (!path || typeof path !== 'string') return false;
// Unix/Linux/macOS 绝对路径
if (path.startsWith('/')) return true;
// Windows 绝对路径
if (/^[A-Za-z]:[\\/]/.test(path)) return true;
return false;
}
// 修复图片路径
function fixImagePaths() {
const imgTags = document.getElementsByTagName('img');
Array.from(imgTags).forEach(function(imgTag) {
const src = imgTag.getAttribute('src');
const dataSrc = imgTag.getAttribute('data-src');
// 跳过 HTTP 图片
if (src && (src.startsWith('http://') || src.startsWith('https://'))) {
return;
}
// 如果 data-src 是绝对路径,直接使用
if (dataSrc && isAbsolutePath(dataSrc)) {
const fixedUrl = 'file://' + dataSrc.replace(/\\/g, '/');
imgTag.setAttribute('src', fixedUrl);
}
});
}
// 拦截 parse_images_BANG_ 函数
function patchImageParsing() {
if (typeof markright === 'undefined' ||
typeof markright.components === 'undefined' ||
typeof markright.components.markdown === 'undefined' ||
typeof markright.components.markdown.parse_images_BANG_ === 'undefined') {
setTimeout(patchImageParsing, 100);
return;
}
const originalParseImages = markright.components.markdown.parse_images_BANG_;
// 包装原函数
markright.components.markdown.parse_images_BANG_ = function(currentPath) {
originalParseImages.call(this, currentPath);
setTimeout(fixImagePaths, 10);
};
// 修复现有图片
fixImagePaths();
}
// 使用 MutationObserver 监控新添加的图片
const observer = new MutationObserver(function(mutations) {
let hasNewImages = false;
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
(node.tagName === 'IMG' || node.querySelectorAll('img').length > 0)) {
hasNewImages = true;
}
});
}
});
if (hasNewImages) {
setTimeout(fixImagePaths, 100);
}
});
// 启动监控和补丁
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(patchImageParsing, 500);
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
});
} else {
setTimeout(patchImageParsing, 500);
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
})();
2.2 在 index.html 中加载补丁
<script>
const script = document.createElement('script');
script.src = 'js/front.js';
script.onload = function() {
// 加载图片路径修复补丁
const patchScript = document.createElement('script');
patchScript.src = 'js/image-path-fix.js';
document.body.appendChild(patchScript);
};
document.body.appendChild(script);
</script>
3. Shell 模块 Polyfill
问题分析
MarkRight 在链接的 onclick 处理器中使用 require('shell').openExternal(...),但在渲染进程中 shell 模块不可用。
解决方案
3.1 在 preload.js 中添加 shell polyfill
const { ipcRenderer } = require('electron');
// 创建 shell 模块 polyfill
const shell = {
openExternal: function(url) {
console.log('[HarmonyOS Preload] shell.openExternal() called:', url);
// 通过 IPC 调用主进程
ipcRenderer.send('electron-shell-openExternal', url);
}
};
// 拦截 require('shell')
const Module = require('module');
const originalRequire = Module.prototype.require;
Module.prototype.require = function(id) {
if (id === 'shell') {
return shell;
}
return originalRequire.apply(this, arguments);
};
// 提供 window.require 供 onclick 处理器使用
if (typeof window !== 'undefined') {
window.shell = shell;
window.require = function(id) {
if (id === 'shell') return shell;
if (id === 'ipc') return ipc;
if (typeof require !== 'undefined') {
return require(id);
}
throw new Error(`Cannot find module '${id}'`);
};
}
3.2 在主进程中添加 IPC 处理器
// 在 main.js 中
ipcMain.on('electron-shell-openExternal', (event, url) => {
try {
console.log('[HarmonyOS IPC] shell.openExternal called:', url);
const { shell } = require('electron');
shell.openExternal(url);
} catch (error) {
console.error('[HarmonyOS IPC] Error in shell.openExternal:', error);
}
});
4. BrowserWindow 拦截
问题分析
需要确保所有 BrowserWindow 实例都正确注入 preload.js 并设置必要的 webPreferences。
解决方案
4.1 创建 HarmonyOSBrowserWindow 包装类
// 在 main.js 中
class HarmonyOSBrowserWindow {
constructor(options) {
const opts = options || {};
// 确保 webPreferences 存在
if (!opts.webPreferences) {
opts.webPreferences = {};
}
// 设置必要的配置
opts.webPreferences.nodeIntegration = true;
opts.webPreferences.contextIsolation = false;
opts.webPreferences.webSecurity = false;
// 注入 preload 脚本
const preloadPath = path.join(app.getAppPath(), 'preload.js');
opts.webPreferences.preload = preloadPath;
// 创建原始 BrowserWindow
const win = new BrowserWindow(opts);
// 设置 MarkRight IPC 目标
if (global.__createMarkRightIPCWrapper) {
const ipcWrapper = global.__createMarkRightIPCWrapper(win);
if (global.electron && global.electron.ipc) {
global.electron.ipc.cljs$core$vreset_BANG_(
global.electron.ipc._STAR_ipc_target_STAR_,
ipcWrapper
);
}
}
return win;
}
}
// 导出到全局作用域
global.HarmonyOSBrowserWindow = HarmonyOSBrowserWindow;
4.2 在 electron-polyfill.js 中使用包装类
let BrowserWindow = require('electron').BrowserWindow;
if (global.HarmonyOSBrowserWindow) {
BrowserWindow = global.HarmonyOSBrowserWindow;
}
const BrowserWindowPolyfill = function(options) {
const opts = options || {};
if (!opts.webPreferences) {
opts.webPreferences = {};
}
opts.webPreferences.nodeIntegration = true;
opts.webPreferences.contextIsolation = false;
opts.webPreferences.webSecurity = false;
// 使用 HarmonyOSBrowserWindow(如果可用)
const win = new BrowserWindow(opts);
// 添加兼容方法
if (!win.loadUrl) {
win.loadUrl = function(url) {
return this.loadURL(url);
};
}
return win;
};
📊 功能测试结果
所有功能已通过完整测试:
| 测试项目 | 状态 | 说明 |
|---|---|---|
| 文件打开 | ✅ | 菜单打开、拖放打开 |
| 文件保存 | ✅ | 保存、另存为 |
| 未保存提示 | ✅ | 关闭窗口时提示 |
| 编辑预览 | ✅ | 实时预览更新 |
| 相对路径图片 | ✅ | 正常显示 |
| 绝对路径图片 | ✅ | 已修复 |
| HTTP 图片 | ✅ | 网络图片加载 |
| Markdown 链接 | ✅ | 外部浏览器打开 |
| Help 菜单链接 | ✅ | GitCode 链接正常 |
| 文件拖放 | ✅ | 单个/多个文件 |
测试进度:12/12 已完成 (100%)
🔍 关键技术细节
1. ClojureScript IPC 消息格式
MarkRight 使用 ClojureScript 的 pr-str 进行消息序列化,消息格式如下:
;; Cast 消息(不需要回复)
{:electron.ipc/op :cast, :electron.ipc/action :load-file, :file "path/to/file.md", :content "..."}
;; Call 消息(需要回复)
{:electron.ipc/op :call, :electron.ipc/action :get-current-content, :electron.ipc/ref #uuid "..."}
2. IPC 目标设置
MarkRight 使用 electron.ipc.set_target_BANG_() 设置 IPC 目标窗口。在 HarmonyOS 适配中,我们通过以下方式实现:
// 创建 IPC 包装器
global.__createMarkRightIPCWrapper = function(win) {
return {
send: function(channel, msg) {
if (win.webContents && !win.webContents.isDestroyed()) {
win.webContents.send(channel, msg);
}
}
};
};
3. 消息队列机制
由于 MarkRight 的 electron.ipc 可能在 IPC 消息到达时尚未加载,我们实现了消息队列机制:
// 如果 electron.ipc 未加载,将消息加入队列
if (!electronIPC || typeof electronIPC.process_msg !== 'function') {
if (!global.__markrightIPCMessageQueue) {
global.__markrightIPCMessageQueue = [];
}
global.__markrightIPCMessageQueue.push({ event, message });
}
// MarkRight 加载后处理队列
if (global.__markrightIPCMessageQueue && global.__markrightIPCMessageQueue.length > 0) {
const queue = [...global.__markrightIPCMessageQueue];
global.__markrightIPCMessageQueue = [];
queue.forEach(({ event, message }) => {
ipcMain.emit('electron.ipc', event, message);
});
}
🐛 常见问题与解决方案
问题 1:图片不显示
症状: 绝对路径图片不显示
原因: MarkRight 的图片处理逻辑错误拼接了绝对路径
解决方案: 使用 image-path-fix.js 补丁脚本检测并修复绝对路径
问题 2:链接点击无反应
症状: 点击 Markdown 链接无反应,控制台报错 Cannot find module 'shell'
原因: 渲染进程中 shell 模块不可用
解决方案: 在 preload.js 中添加 shell 模块 polyfill,通过 IPC 调用主进程
问题 3:IPC 消息未处理
症状: IPC 消息发送后无响应
原因: electron.ipc 对象尚未加载
解决方案: 实现消息队列机制,在 MarkRight 加载后处理队列中的消息
📈 性能优化
1. 延迟加载
- MarkRight 的主进程代码在
app.whenReady()之前加载,确保事件监听器正确注册 - 图片路径修复补丁在
front.js加载后加载,避免时序问题
2. 消息队列
- 使用消息队列避免消息丢失
- 在 MarkRight 加载完成后批量处理队列消息
3. MutationObserver
- 使用
MutationObserver实时监控 DOM 变化 - 只在有新图片添加时才执行修复逻辑
🎓 经验总结
1. 理解应用架构
在适配 Electron 应用到 HarmonyOS 之前,需要深入理解应用的架构:
- 主进程和渲染进程的通信方式
- IPC 消息格式和序列化方式
- 依赖的 Node.js 模块和 native 模块
2. 渐进式适配
采用渐进式适配策略:
- 先实现基本的 IPC 通信
- 逐步添加功能支持
- 修复发现的问题
- 优化性能和体验
3. 补丁机制
对于无法直接修改的编译后代码,使用补丁机制:
- 在运行时拦截和修改函数
- 使用
MutationObserver监控 DOM 变化 - 提供 polyfill 替代不可用的模块
4. 完整测试
确保所有功能都经过完整测试:
- 文件操作(打开、保存、另存为)
- 图片显示(相对路径、绝对路径、HTTP)
- 链接功能(Markdown 链接、菜单链接)
- 文件拖放
🔗 相关资源
📝 更新日志
v1.0.0 (2024-11-28)
- ✅ 完成 MarkRight 到 HarmonyOS PC 的完整适配
- ✅ 实现自定义 IPC 通信系统
- ✅ 修复绝对路径图片显示问题
- ✅ 实现 Shell 模块 polyfill
- ✅ 更新 Help 菜单链接为 GitCode
- ✅ 所有功能测试通过
Made with ❤️ for HarmonyOS PC
更多推荐


所有评论(0)