Electron for 鸿蒙PC:用 Node-API 打通原生系统调用,告别"Web 孤岛"
前言
把 Electron 应用移植到鸿蒙 PC 平台,最大的拦路虎不是 UI,而是原生系统能力。
在 Windows 上,你可以通过 node-ffi、winapi 或 Shell 命令随意调用系统功能。但鸿蒙 PC 的系统 API 是 C/C++ 接口,Electron 的 Node.js 运行时无法直接触达。解法只有一个:Node-API(原 N-API)——用 C++ 编写原生插件,再通过 require() 暴露给 Electron 主进程。
本文将从零开始,带你完成以下实战目标:
- 搭建 Electron for HarmonyOS 的 Node-API 开发环境
- 实现文件系统监控(
inotify 风格 Watch)
一、技术架构总览
┌─────────────────────────────────────────────────┐│ Electron 渲染进程 (Chromium) ││ Vue/React SPA ──IPC(contextBridge)──▶ 主进程 │└──────────────────────────┬──────────────────────┘ │ require('./addon.node')┌──────────────────────────▼──────────────────────┐│ Node.js 主进程 (Node-API 层) ││ addon.node ──C++ FFI──▶ 鸿蒙系统 C API │└─────────────────────────────────────────────────┘
核心链路:渲染进程 → IPC → Node.js 主进程 → Node-API 插件(.node)→ 鸿蒙原生 C API
二、环境搭建
2.1 前置要求
2.2 获取 Electron for HarmonyOS
# 克隆官方适配仓库git clone https://gitee.com/openharmony-tpc/electron.gitcd electrongit checkout ohos-main # 切到鸿蒙分支# 安装专用 npm 包(内含鸿蒙 SDK 路径)npm install @ohos/electron-napi-headers --save-dev
2.3 配置 node-gyp 指向鸿蒙 SDK
# 设置环境变量(以 DevEco Studio 默认路径为例)export OHOS_SDK_HOME=OHOS_SDK_HOME"# 验证node-gyp list
三、编写第一个 Node-API 插件:获取系统硬件信息
3.1 目录结构
electron-hmos-demo/├── native/│ ├── binding.gyp # node-gyp 构建配置│ ├── sysinfo.cpp # 原生插件主体│ └── include/│ └── ohos_sysinfo.h # 鸿蒙系统信息头文件├── main.js # Electron 主进程├── preload.js # 预加载脚本├── renderer/│ └── index.html # 渲染页面└── package.json
3.2 编写原生插件 sysinfo.cpp
// native/sysinfo.cpp#include<napi.h>#include<string>// 鸿蒙 PC 系统信息 API(ohos.systemCapability.deviceInfo)#ifdef __OHOS__#include"device_info.h"// HarmonyOS SDK 头文件#else// Windows/Mac 开发环境 mock#include<sys/utsname.h>#endif// 获取设备型号Napi::String GetDeviceModel(const Napi::CallbackInfo& info){ Napi::Env env = info.Env();#ifdef __OHOS__ // 鸿蒙原生 API:OH_DeviceInfo_GetDeviceModel char model[256] = {0}; OH_DeviceInfo_GetDeviceModel(model, sizeof(model)); return Napi::String::New(env, std::string(model));#else // 非鸿蒙环境返回 mock 数据 return Napi::String::New(env, "HarmonyOS-PC-Mock");#endif}// 获取系统版本Napi::String GetOsVersion(const Napi::CallbackInfo& info){ Napi::Env env = info.Env();#ifdef __OHOS__ char version[128] = {0}; OH_DeviceInfo_GetSystemVersion(version, sizeof(version)); return Napi::String::New(env, std::string(version));#else struct utsname buf; uname(&buf); return Napi::String::New(env, std::string(buf.release));#endif}// 获取可用内存(MB)Napi::Number GetFreeMemory(const Napi::CallbackInfo& info){ Napi::Env env = info.Env();#ifdef __OHOS__ // 鸿蒙 OH_Memory API OH_MemoryInfo_t memInfo; OH_Memory_GetInfo(&memInfo); double freeMB = static_cast<double>(memInfo.freeBytes) / 1024.0 / 1024.0; return Napi::Number::New(env, freeMB);#else // 开发环境用 Node.js 内置 API mock return Napi::Number::New(env, 4096.0);#endif}// 异步获取 CPU 使用率(演示异步 Node-API 用法)class CpuUsageWorker : public Napi::AsyncWorker {public: CpuUsageWorker(Napi::Function& callback) : Napi::AsyncWorker(callback), cpuUsage_(0.0) {} voidExecute()override{#ifdef __OHOS__ OH_CpuInfo_t cpuInfo; OH_Cpu_GetUsage(&cpuInfo); cpuUsage_ = cpuInfo.totalUsage * 100.0;#else cpuUsage_ = 23.5; // mock#endif } voidOnOK()override{ Napi::HandleScope scope(Env()); Callback().Call({Env().Null(), Napi::Number::New(Env(), cpuUsage_)}); }private: double cpuUsage_;};Napi::Value GetCpuUsageAsync(const Napi::CallbackInfo& info){ Napi::Env env = info.Env(); if (info.Length() < 1 || !info[0].IsFunction()) { Napi::TypeError::New(env, "Callback function required").ThrowAsJavaScriptException(); return env.Null(); } Napi::Function callback = info[0].As<Napi::Function>(); auto* worker = new CpuUsageWorker(callback); worker->Queue(); return env.Undefined();}// 模块初始化 & 导出Napi::Object Init(Napi::Env env, Napi::Object exports){ exports.Set("getDeviceModel", Napi::Function::New(env, GetDeviceModel)); exports.Set("getOsVersion", Napi::Function::New(env, GetOsVersion)); exports.Set("getFreeMemory", Napi::Function::New(env, GetFreeMemory)); exports.Set("getCpuUsageAsync", Napi::Function::New(env, GetCpuUsageAsync)); return exports;}NODE_API_MODULE(sysinfo, Init)
3.3 编写 binding.gyp
{ "targets": [{ "target_name": "sysinfo", "sources": ["sysinfo.cpp"], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")", "<(OHOS_SDK_HOME)/native/sysapi/include" ], "libraries": [ "-L<(OHOS_SDK_HOME)/native/sysapi/lib", "-ldevice_info", "-lmemory_info", "-lcpu_info" ], "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"], "cflags_cc": ["-std=c++17"], "conditions": [ ["OS=='ohos'", { "defines": ["__OHOS__=1"] }] ] }]}
3.4 编译插件
cd native# 首次编译node-gyp configure --arch=x64 --target_platform=ohosnode-gyp build# 输出产物:native/build/Release/sysinfo.node
四、在 Electron 主进程中调用插件
4.1 主进程 main.js
// main.jsconst { app, BrowserWindow, ipcMain } = require('electron');const path = require('path');// 加载原生插件(自动适配 Debug/Release)let sysinfo;try { sysinfo = require('./native/build/Release/sysinfo.node'); console.log('[Node-API] sysinfo plugin loaded successfully');} catch (err) { console.error('[Node-API] Failed to load sysinfo plugin:', err.message); // Fallback: 使用 JS 层 mock(开发阶段) sysinfo = { getDeviceModel: () => 'HarmonyPC-DEV', getOsVersion: () => '5.0.0-OHOS', getFreeMemory: () => 8192, getCpuUsageAsync: (cb) => setTimeout(() => cb(null, 15.8), 100) };}function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, // 必须开启! nodeIntegration: false, // 必须关闭! } }); win.loadFile('renderer/index.html');}app.whenReady().then(createWindow);// ===== IPC 处理:同步系统信息查询 =====ipcMain.handle('get-sys-info', async () => { return { deviceModel: sysinfo.getDeviceModel(), osVersion: sysinfo.getOsVersion(), freeMemoryMB: sysinfo.getFreeMemory(), };});// ===== IPC 处理:异步 CPU 使用率 =====ipcMain.handle('get-cpu-usage', () => { return new Promise((resolve, reject) => { sysinfo.getCpuUsageAsync((err, usage) => { if (err) reject(err); else resolve(usage); }); });});
4.2 预加载脚本 preload.js
// preload.js - 安全地将 IPC 能力暴露给渲染进程const { contextBridge, ipcRenderer } = require('electron');contextBridge.exposeInMainWorld('electronAPI', { // 获取系统信息(一次性) getSysInfo: () => ipcRenderer.invoke('get-sys-info'), // 获取 CPU 使用率 getCpuUsage: () => ipcRenderer.invoke('get-cpu-usage'), // 订阅实时 CPU 更新(推模式) onCpuUpdate: (callback) => { ipcRenderer.on('cpu-update', (_event, value) => callback(value)); // 返回取消订阅函数 return () => ipcRenderer.removeAllListeners('cpu-update'); }});
五、渲染进程展示数据
<!-- renderer/index.html --><!DOCTYPE html><htmllang="zh"><head> <metacharset="UTF-8"> <title>鸿蒙 PC 系统监控</title> <style> body { font-family: 'HarmonyOS Sans', sans-serif; background: #0d1117; color: #e6edf3; padding: 2rem; } .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; } .metric { font-size: 2rem; font-weight: bold; color: #58a6ff; } .label { font-size: 0.85rem; color: #8b949e; margin-top: 0.25rem; } #cpu-bar { height: 8px; background: #21262d; border-radius: 4px; margin-top: 0.5rem; } #cpu-fill { height: 100%; background: linear-gradient(90deg, #238636, #3fb950); border-radius: 4px; transition: width 0.5s; }</style></head><body> <h1>🖥️ HarmonyOS PC 系统监控</h1> <divclass="card"> <divclass="label">设备型号</div> <divclass="metric"id="device-model">加载中...</div> </div> <divclass="card"> <divclass="label">系统版本</div> <divclass="metric"id="os-version">加载中...</div> </div> <divclass="card"> <divclass="label">可用内存</div> <divclass="metric"id="free-mem">加载中...</div> </div> <divclass="card"> <divclass="label">CPU 使用率</div> <divclass="metric"id="cpu-usage">0%</div> <divid="cpu-bar"><divid="cpu-fill"style="width:0%"></div></div> </div> <script> (async () => { // 一次性获取系统信息 const info = await window.electronAPI.getSysInfo(); document.getElementById('device-model').textContent = info.deviceModel; document.getElementById('os-version').textContent = info.osVersion; document.getElementById('free-mem').textContent = `{usage.toFixed(1)}%`; document.getElementById('cpu-fill').style.width = `${usage}%`; }; await refreshCpu(); setInterval(refreshCpu, 2000); // 每 2 秒刷新 })();</script></body></html>
六、打包部署到鸿蒙 PC
6.1 package.json 关键配置
{ "name": "electron-hmos-sysinfo", "version": "1.0.0", "main": "main.js", "scripts": { "build:native": "cd native && node-gyp rebuild --target_platform=ohos", "pack:hmos": "electron-builder --platform=ohos --arch=x64", "dev": "electron ." }, "build": { "appId": "com.example.electron.sysinfo", "productName": "SysMonitor", "ohos": { "target": "default", "hap": { "type": "entry", "bundleName": "com.example.electron.sysinfo" } }, "files": [ "main.js", "preload.js", "renderer/**", "native/build/Release/*.node" ] }, "dependencies": { "node-addon-api": "^8.1.0" }, "devDependencies": { "electron": "34.0.0-ohos", "electron-builder": "^25.0.0" }}
6.2 打包命令
# 1. 编译原生插件(针对鸿蒙架构)npm run build:native# 2. 打包为鸿蒙 HAP 包npm run pack:hmos# 3. 通过 DevEco Studio 安装到鸿蒙 PChdc shell install dist/com.example.electron.sysinfo-1.0.0.hap
七、踩坑指南
坑1:.node 文件 RPATH 问题
鸿蒙 PC 的动态库搜索路径与 Linux 不同,需在 binding.gyp 中显式设置:
"ldflags": ["-Wl,-rpath,@loader_path/../lib"]
坑2:异步 Worker 线程崩溃
Node-API 的 AsyncWorker::Execute() 运行在 libuv 线程池中,不能在此直接调用任何 Napi 对象。如需传递数据,必须用成员变量暂存,在 OnOK() 中再转换为 JS 值。
坑3:contextIsolation 导致 require 不可用
渲染进程绝对不能用 require('electron'),必须通过 preload.js + contextBridge 传递能力。违反此规则会导致安全漏洞,且在新版 Electron 中直接报错。
坑4:鸿蒙 SDK 版本不匹配
OH_DeviceInfo_GetDeviceModel 在 HarmonyOS NEXT DB6 之前的 API Level 不存在,确保 sdk-version 对齐:
// module.json5"metadata": [ { "name": "ohos.SDK.version", "value": "12" }]
八、总结与扩展方向
通过 Node-API,Electron 应用获得了直接调用鸿蒙 PC 原生 C API 的能力,从根本上解决了"Web 孤岛"问题。
下一步推荐阅读:
- 官方仓库:
https://gitcode.com/openharmony-sig/electron - Node-API 文档:
nodejs.org/api/n-api.html - 应用如何接入Electron框架:
https://developer.huawei.com/consumer/cn/blog/topic/03208728403662067
💡 如果本文对你有帮助,欢迎点赞收藏!如有疑问欢迎评论区留言。