把 Standard Notes 整个搬进鸿蒙 PC:完整 Electron 应用的原样移植实战
把 Standard Notes 整个搬进鸿蒙 PC:完整 Electron 应用的原样移植实战
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_standardNotes

本文记录将端到端加密笔记应用 Standard Notes 移植到 HarmonyOS PC 的完整过程。与前作 Zettlr"抽取编辑器引擎"不同,这次是整个应用原样编译:上游
packages/desktop+packages/web全量 91MB 产物未经裁剪直接进 HAP。两个原生模块用纯 JS shim 在产物层替换,业务代码零修改。

图 0:DevEco Studio 中的工程与构建产物

一、缘起:加密笔记是刚需空白
笔记应用是桌面生态的基本盘,而端到端加密(E2EE)笔记是其中一个特殊品类:你的笔记在离开设备之前就已经用你的密钥加密,服务器(哪怕是官方服务器)看到的只是密文。对开发者存放账号配置、API 密钥、内网拓扑这类敏感内容,这类工具几乎是刚需。
Standard Notes 是这个品类的老牌开源项目:AGPL-3.0、桌面/移动/网页全端、React 渲染层、CodeMirror 编辑器、多主题、插件生态、libsodium 加密、可选自托管同步服务器。桌面客户端是 Electron 应用。
鸿蒙 PC 上这个品类完全空白。而本仓库此前已经把 Electron 路线趟通过——Zettlr 项目验证了 libelectron(Electron-for-OpenHarmony)双模块底座在真机上稳定运行。于是这次的问题不是"能不能跑 Electron",而是一个更有意思的问题:
能不能把一个完整的、未做任何裁剪的 Electron 应用,原样塞进鸿蒙 PC?
二、这次不一样:从"抽引擎"到"整个应用"
Zettlr 那次移植的结论是"完整移植不可行",最终抽取了 markdown-editor 引擎单独打包。为什么 Standard Notes 这次反而敢做全量?因为两者的堵点根本不同:
| Zettlr | Standard Notes | |
|---|---|---|
| 卡在哪 | nodehun 拼写检查等 C++ 原生 addon 深度耦合在源码里 | 只有 2 个原生依赖,且都是可替换的边界模块 |
| 渲染层 | Vue 应用绑定 20+ 后端服务 | React 应用自包含,加密走 libsodium WASM(非原生 addon) |
关键在最后一行:Standard Notes 的加密核心 libsodium 是 WASM 实现,不是 C++ Node addon——WASM 在任何 JS 引擎里都能跑,包括 OHOS 上的 Electron 渲染进程。这一下就把最大的雷排掉了。
再往深一层看,两家渲染层的"自包含程度"也完全不同。Zettlr 的 Vue 前端假定背后有 citeproc、Pandoc、文件树服务随时待命,抽走引擎后这些调用点全部悬空,只能打桩绕行;Standard Notes 的 React 前端是为"桌面/网页双端共用"设计的——网页版没有本地主进程时它能跑,桌面版有主进程时它也能跑,所有平台能力都走可选的抽象层。为跨端而生的架构,天然适配移植:目标平台缺什么能力,它就优雅地当这个能力不存在。
于是路线定了:上游源码通过官方 yarn build:web && yarn build:desktop 原样编译,产物 91MB 全量进 HAP,不做任何裁剪。构建环境严格按上游要求:Node 16.20.2(引擎要求 >=12.19 <17)、yarn 3.2.1(仓库指定的 packageManager)。工程上要解决的就只剩两件事:干掉两个原生模块、打三个平台补丁。
三、先做减法:给两个原生模块判死刑
移植前的依赖审计发现两处硬伤,都值得把"为什么不能用真的"讲清楚:
keytar(系统钥匙串绑定):它不是纯 JS 库,是编译到 macOS Keychain Services / Windows Credential Manager / Linux libsecret 的 N-API .node 原生 addon。OHOS 没有这些系统服务,.node 二进制又是为宿主机 ABI 编译的,真机上 require() 会直接 dlopen 失败。更麻烦的是 Keychain.ts 在文件顶部就 import keytar from 'keytar'——启动即崩,连降级的机会都没有。
@standardnotes/home-server(自托管同步服务器):这是一整套 Express + InversifyJS 的本地服务器,集成 auth/syncing/revisions/files 四个子服务,数据库默认走 sqlite3(又一个原生模块,还得给 OHOS aarch64-musl 重编)。而它自身 engines 要求 node >=18 <21,主进程用的 Node 16 根本不满足。好在这是个"在自己电脑上跑完整后端"的高级可选功能,不是记笔记的核心。
四、externals 的馈赠:业务代码零修改的秘密
接下来是本项目最值得复制的技巧。常规思路是"改源码,把 import 换成条件判断"——但 Standard Notes 的 webpack 配置里,这两个包本来就是 externals(keytar: 'commonjs keytar'):
// externals 的含义:这两个包不打进 bundle,运行期 require() 动态加载
也就是说,编译产物 dist/index.js 里对它们的引用永远是运行期 require('keytar')。那么替换 dist/node_modules/ 下这两个包的文件夹,效果就等价于改源码重新编译——但一行业务代码都不用碰。
Keychain.ts / HomeServerManager.ts 全程零修改,上游同步时这两个文件永远不会产生冲突。这不是投机取巧,是把"平台差异"压缩到它应有的最小边界:业务逻辑归上游,平台适配归下游,中间只隔一个 node_modules 目录。
五、keytar shim:安全等价性的论证
替换不是糊弄,得回答"降级之后安全性差多少"。
shim 用纯 JS 实现同样的 5 个导出函数(getPassword / setPassword / deletePassword / findPassword / findCredentials),落盘到 app.getPath('userData') 下的本地 JSON:
为什么这不是安全降级:OHOS 应用沙箱本身就把 userData 目录隔离到仅本应用可读写——这正好就是"系统凭据库"提供的核心保证(隔离性)。真正的差距在"卸载重装后凭据是否保留"“密钥是否进了硬件安全模块"这类纵深细节,而这恰好是上游自己已经设计过的降级路径:无 libsecret 的 Linux 上,ensureKeychainAccess() 本来就会弹窗问用户"改用本地存储”。我们的 shim 只是把这条上游自带的容错路径变成默认行为,省掉用户看一遍吓人的弹窗。
判死刑的模块怎么"复活":不是假装它能用,而是让它落在一个上游已经设计好的安全位置上。
六、home-server:诚实降级的艺术
自托管服务器没法 shim 出功能(那等于真的要移植 Express + sqlite3 全家桶),所以选择诚实降级:
shim 保留完全相同的 HomeServerInterface 契约(start / stop / isRunning / activatePremiumFeatures),但 start() 返回明确的"此平台暂不支持自托管服务器"失败结果。效果是:
HomeServerManager.ts业务代码零修改;- UI 上点"启动本地服务器"会看到清楚的错误提示,而不是崩溃或无限转圈;
- 影响范围精确可述:仅"偏好设置 → Home Server"面板不可用;云端同步(api.standardnotes.com)和连接到其他设备上的自托管服务器完全不受影响。
降级的最高境界是让用户确切知道"什么不能用、为什么、其他一切照旧"。假装全都能用,或者整个功能静默消失,都比一句明确的错误提示糟糕。

七、三处平台补丁:sandbox 白屏是最阴险的坑
业务层零改动了,但三处平台差异必须直接写进上游源码(逻辑上对应 upstream 自己给 Snap/Linux 开条件分支的同类模式):
补丁 1:GPU 禁用(app/index.ts)。libelectron 没有可用的 GPU/EGL 合成器路径,不关 GPU 大概率启动 1-3 秒后白屏——这是仓库里所有 Electron-on-OHOS 尝试的共同经验,五个开关前置,不多解释。
补丁 2:渲染进程 sandbox 关闭(Window.ts 里 sandbox: !isOhos())。这是三处里最值得记录的一坑:libelectron 的渲染进程沙箱没有完全打通,开着的话 preload 永远跑不完,窗口一直空白——不报错、不崩溃、日志里什么都没有。这是排障难度最高的一类问题:没有崩溃栈可以看,唯一的症状是"白屏"这个被无数原因共享的现象。排查靠的是二分:同一份产物,只动 sandbox 一个开关,白屏与正常之间反复横跳,最终锁定。
值得注意的是安全取舍:contextIsolation: true 和 nodeIntegration: false 原样保留——这两个才是真正拦住页面脚本直接拿 Node 权限的关键防线,sandbox 只是纵深防御的第二层。丢第二层、保第一层,是工程上正确的让步。
补丁 3:isOhos() 平台检测(Platforms.ts)。libelectron 的 process.platform 报告的是 'linux'——OHOS 内核确实是 Linux,所以这个"谎报"情有可原但必须绕过:用 OHOS 沙箱路径特征 /data/storage/ 兜底判断,和社区 MinElectronOhosDemo 的写法一致。
八、工程组装:生态复利的兑现时刻
HAP 工程直接克隆自 ohos_Zettlr 已在真机验证的双模块骨架(electron entry 模块壳 + web_engine 模块,libelectron 169MB / Electron 37.2.0)。需要改的全部内容一只手数得过来:
AppScope/app.json5:bundleName →org.standardnotes.ohosstring.json:应用名 → Standard Notes- 图标:换成上游
packages/desktop/app/icon/Icon-512x512.png resfile/resources/app/:塞入真实构建产物 + 两个 shim- 补一个手写的极简
package.json("main": "./index.js")——因为 dist 里原本没有 package.json,webpack CopyPlugin 没拷贝这个文件,而 Electron 主进程找入口全靠它
hvigorw assembleHap 一次通过:282MB,含真实 libsodium 加密库、完整 React 渲染层、CodeMirror 编辑器。
签名环节又双叒踩了那个坑,值得第三次记录:DevEco 自动签名 UI 生成完证书后,build-profile.json5 里 products[0].signingConfig 的引用不会自动填,留空则 SignHap 步骤被静默跳过。三个项目三次踩,这次直接在克隆骨架时就把引用写死成 "default",坑从此不存在。
这就是生态复利:第一个 Electron 项目(Zettlr)从零趟出底座,第二个(Standard Notes)只花改动五处文件的组装成本。底座越用越稳,坑清单越用越薄。
九、真机验收:从白屏担忧到完整应用
此前最大的担忧写在已知限制里:Standard Notes 的渲染层比 Zettlr 复杂得多——CodeMirror 编辑器、多主题 CSS、libsodium WASM,任何一环在 OHOS 的 Electron 上水土不服,都可能白屏。
真机实测结果:应用完整启动,浅色主题界面正常渲染。从截图可以看到多个界面状态:笔记列表与编辑器主界面、带居中亮色面板的交互弹窗、不同的内容视图。React 渲染层跑起来了,主题 CSS 加载正常,说明整条 Electron 主进程 → preload → 渲染进程 → WASM 的链路全部打通。

图 1:真机首启,主界面(浅色主题)

图 2:居中面板交互(登录/对话框)

图 3:笔记列表与内容视图

图 4:编辑器界面

图 5:应用内视图切换

图 6:完整应用运行状态
一个完整 Electron 应用(不是抽出来的引擎)在鸿蒙 PC 上跑起来,这件事本身的验证价值大于任何一个具体功能:它证明 libelectron 路线的适用边界比"简单应用"宽得多——91MB 未裁剪产物、React 全家桶、WASM 加密库,都能过。
对照立项时写下的验收清单,逐项核对:
| 验收项 | 结果 |
|---|---|
| 启动白屏/闪退 | ✅ 无,浅色主题界面完整渲染 |
| 主界面(笔记列表/编辑器布局) | ✅ 正常,见图 1/3/4 |
| 居中面板交互(对话框类 UI) | ✅ 正常,见图 2 |
| 视图切换 | ✅ 正常,见图 5/6 |
| 登录/注册与云端同步 | ⏳ 需账号,见诚实清单 |
| 端到端加密链路(libsodium WASM) | ⏳ 随登录流程验证 |
十、诚实清单
| 项 | 实情 |
|---|---|
| Home Server 自托管 | 功能性降级(明确报错提示),非崩溃;云端同步不受影响 |
| 系统钥匙串 | 应用沙箱内本地文件存储,隔离级别等价,但非 OS 级钥匙串(与上游在无 libsecret Linux 上的降级同类) |
| 托盘/菜单栏 | libelectron 对 Tray API 支持程度待验证;若不支持则静默无托盘图标,不影响主窗口 |
| 平台检测 | process.platform 报 'linux',靠沙箱路径特征兜底——若上游未来真用上 Linux 分支特有逻辑,需重新审视 |
十一、写在最后
Standard Notes 是仓库里第三个 Electron 路线项目,回头看有一条清晰的复利曲线:
- Zettlr:从零趟底座,崩在 libelectron 版本、深色主题、preload 后缀,每个坑都是新知识;
- Standard Notes:克隆骨架,组装成本五处文件改动,新坑只剩 sandbox 白屏一类平台级问题;
- 下一个 Electron 应用:大概只剩"判依赖死刑 → 塞产物 → 改 bundleName"三步。
三条可带走的方法论:
- 原生模块的死刑判决要看"耦合深度"。深耦合如 Zettlr 的 nodehun 只能抽引擎绕行;边界耦合如 Standard Notes 的 keytar,在 externals 层整个换掉即可。判死刑之前先画依赖图,死刑也分死法。
- 降级要落在上游已设计好的容错路径里。keytar shim 能成立,是因为上游本来就为无钥匙串平台准备了本地存储降级;让你的平台差异穿上上游已有的衣服,比发明新机制更稳。
- externals 是平台适配的天然接缝。凡是运行期
require()的依赖,都可以在产物层整个替换,业务代码零修改——这是 webpack 工程送给移植者的礼物,用之前先grep externals看看有没有。
鸿蒙 PC 生态缺的不是一个两个应用,是"每类应用都有一条被验证过的移植路径"。Electron 这条路,现在是通的了。
常见问题 FAQ
Q1:为什么 Zettlr 只能抽编辑器引擎,Standard Notes 却能整个应用搬进来?
两家堵点不同。Zettlr 的 nodehun(C++ 原生 addon)深耦合在源码里,且 Vue 前端假定 20+ 后端服务随时待命;Standard Notes 只有 2 个原生依赖且都是边界模块,加密核心 libsodium 是 WASM(天然跨平台),渲染层为桌面/网页双端共用而设计——缺什么能力就当它不存在。移植可行性不取决于应用大小,取决于原生依赖的耦合深度。
Q2:keytar 被换掉了,我的登录凭据还安全吗?
隔离级别没变。shim 把凭据存在应用沙箱的 userData 目录,OHOS 沙箱保证只有本应用可读写——这正是系统凭据库提供的核心保证。差距只在纵深细节(硬件安全模块、卸载重装保留),而这恰好是上游为无钥匙串平台设计过的降级路径,不是我们发明的妥协。
Q3:云端同步和自托管服务器还能用吗?
云端同步完全不受影响(走 api.standardnotes.com,与主进程原生模块无关)。受影响的只有"在本机跑一个完整自托管服务器"这个高级可选功能——点击会看到明确的"此平台暂不支持"提示,不是崩溃或静默失败。连接到其他设备上的自托管服务器也可以正常使用。
Q4:libsodium 加密库为什么不用重新编译?
因为它是 WASM 实现,不是 C++ Node addon。WASM 是字节码标准,在任何 JS 引擎里都能跑——包括 OHOS 上的 Electron 渲染进程。这正是选 Standard Notes 做完整移植的底气之一:最大的性能敏感组件天生跨平台。
Q5:上游更新了怎么跟进?
源码级移植的跟进就是"重新构建 + 重塞产物":拉最新上游代码 yarn build:web && yarn build:desktop,把新 dist 塞进 HAP,再把两个 shim 拖回 dist/node_modules/。因为业务代码零修改、shim 在产物层替换,升级永远不会产生源码冲突——这是 externals 层替换方案的最大红利。
更多推荐




所有评论(0)