Electron 调用 C++ 与 Native Addon 学习笔记
适合目标:系统掌握 Electron 中调用 C++ 的完整链路,覆盖 C++ 基础补强、Native Addon 接入方式、Node-API / node-addon-api、构建发布、跨平台适配与高频面试题。
学习重点:Electron 为什么需要 C++、调用链路如何设计、Node-API 和 node-addon-api 的定位、ABI 与重编译、异步任务、跨平台构建与项目实战。
学习原则:先理解为什么要接原生,再理解接入方案差异;先走通最小闭环,再做性能和工程优化;先把安全边界理清,再扩展原生能力。
说明:这篇笔记结合了 2026 年 4 月 21 日我核对的官方资料整理。Electron 官方文档仍明确说明原生模块通常需要为 Electron 重新编译;Node.js 官方文档则继续推荐优先使用
Node-API构建 Addon。
目录
- 学习总览
- Electron 为什么要调用 C++
- Electron 调 C++ 的完整调用链
- 做这件事前必须掌握的 C++ 基础
- Electron 中调用 C++ 的几种方案
- Native Addon 核心概念
- Node-API、node-addon-api、NAN、直接 V8 的区别
- 为什么现在优先 Node-API
- 最小可运行 Native Addon 示例
- Electron 中如何安全暴露 C++ 能力
- 异步任务、线程与性能问题
- 字符串、Buffer、对象、错误处理怎么传
- 构建链路:node-gyp、binding.gyp、@electron/rebuild
- Electron 打包发布时的 Native Addon 注意事项
- 跨平台问题:Windows、macOS、Linux
- 实战架构建议
- 高频面试题
- 学习路线建议
- 官方资料入口
- 一页速记总结
- 背诵口诀
1. 学习总览
1.1 这部分到底在学什么
很多前端同学一看到 Electron 调 C++,容易把它理解成:
- 写个
.node文件 - 在 Electron 里
require - 调一下方法
但真实项目里,这件事远不止这么简单。
你真正学的是:
如何把 Electron 的 JavaScript 世界、Node 原生模块世界和 C++ / 系统能力世界稳定地连接起来。
这里面会涉及:
- C++ 基础类型和内存模型
- Node Native Addon 机制
- Electron 的 ABI 与重编译
- Preload / IPC 的安全边界
- 异步线程与事件循环
- 跨平台编译和打包发布
1.2 学习主线
主线 1:为什么要调用 C++
你要先知道需求来自哪里,而不是为了“炫技”上原生。
主线 2:Addons 怎么嵌进 Electron
Native Addon 如何被 Node / Electron 加载,ABI 为什么会出问题。
主线 3:为什么现在优先 Node-API
这关系到稳定性、升级成本和维护成本。
主线 4:工程上怎么落地
开发环境能跑不代表生产能发,真正难点常常在:
- 编译
- 重建
- 打包
- 签名
- 跨平台
2. Electron 为什么要调用 C++
2.1 常见原因
Electron 本身已经能做很多事情,但仍然有场景必须或非常适合接 C++:
- 调系统底层 API
- 接已有 C / C++ SDK
- 性能敏感计算
- 音视频编解码
- 图像处理
- 硬件设备通信
- 驱动层或厂商库接入
2.2 典型业务场景
- 摄像头、麦克风、串口、USB 设备控制
- 调工业设备、扫码枪、打印机 SDK
- OCR、音视频处理、AI 推理引擎接入
- 高性能本地加密、压缩、特征计算
- 和公司已有 C++ 代码库集成
2.3 为什么不直接全用 JavaScript
因为 JavaScript 并不总能解决:
- 底层系统 API 可达性
- 已有原生 SDK 集成
- 计算密集型性能
- 与操作系统或硬件的紧耦合能力
2.4 但为什么也不能滥用 C++
因为原生接入会增加:
- 开发难度
- 跨平台复杂度
- 调试成本
- 发布成本
- 升级成本
一句话:
Electron 调 C++ 是为了解决 JavaScript 不擅长的问题,不是为了把所有逻辑都搬到原生层。
3. Electron 调 C++ 的完整调用链
最推荐理解的完整链路是:
Renderer UI -> Preload API -> ipcRenderer / 直接 Node 接口 -> Main / Node Runtime -> Native Addon -> C++ / SDK / OS API -> 返回结果
3.1 最常见工程分层
Renderer
只负责:
- UI
- 用户交互
- 展示结果
Preload
负责:
- 向页面暴露白名单 API
- 屏蔽底层实现细节
- 限制页面直接拿到 Node 能力
Main 或 Node 层
负责:
- 调用 Native Addon
- 编排业务流程
- 管理权限和资源
C++ Addon
负责:
- 封装原生 SDK
- 转换数据结构
- 执行高性能逻辑
3.2 为什么不建议 Renderer 直接乱调 Addon
因为这会导致:
- 安全边界弱
- 页面层和底层强耦合
- 测试困难
- 升级困难
更好的思路是:
把 Native Addon 看成基础设施能力,由主进程或专门的 native service 层统一收口。
4. 做这件事前必须掌握的 C++ 基础
如果你的目标是 Electron 调 C++ 开发,C++ 不需要一上来就学到模板元编程,但下面这些基础必须够稳。
4.1 语法基础
- 变量、函数、类
- 引用、指针
- 构造函数、析构函数
- 命名空间
const- 头文件与实现文件分离
4.2 内存与对象生命周期
这是最重要的一块。
必须掌握:
- 栈和堆
new/delete- RAII
- 智能指针
- 对象何时创建、何时销毁
因为 Electron + Native Addon 里最容易踩坑的往往就是:
- 内存泄漏
- 悬空指针
- 重复释放
- 线程竞争导致的对象生命周期异常
4.3 STL 常用容器
至少要熟:
std::stringstd::vectorstd::map/std::unordered_mapstd::optionalstd::unique_ptrstd::shared_ptr
4.4 错误处理
要理解:
- 返回码风格
- 异常风格
- C 接口和 C++ 接口的差异
很多第三方 SDK 不是抛异常,而是返回错误码。
4.5 并发与线程
必须理解:
- 线程和主线程
- 互斥锁
- 条件变量
- 线程安全
- 不要阻塞 Node 主线程
4.6 C++ 对 Electron 开发最重要的能力画像
如果你的目标不是做纯 C++ 工程师,而是做 Electron 原生集成,那么最重要的不是刷复杂语法,而是:
- 能读懂 SDK 头文件
- 能写对象封装
- 能做内存管理
- 能处理线程与异步
- 能把 C++ 数据映射回 JS
5. Electron 中调用 C++ 的几种方案
5.1 Node Native Addon
这是最主流、最标准的方式。
特点:
- 直接在 Node / Electron 运行时里加载
.node模块 - 接口像普通 JS 模块一样调用
- 性能好
- 与 Electron 集成最自然
5.2 调动态库
比如通过:
ffi-napi- 自定义桥接层
调用 .dll、.so、.dylib。
优点:
- 可以直接复用已有库
缺点:
- 类型映射复杂
- 调试难
- 稳定性与维护性可能不如 Addon 封装
5.3 子进程调用本地可执行程序
通过:
child_process.spawnexecFile
调用本地 exe 或命令行程序。
适合:
- 已有独立 CLI 工具
- 进程隔离更重要
缺点:
- 通信成本更高
- 启动耗时更高
- 数据交互不够自然
5.4 Rust / C 包装后再接入
虽然你问的是 C++,但工程上也常见:
- Rust 编译成 Node Addon
- C 库封装成 Addon
这说明:
Electron 调原生的本质是 Native Addon,不一定非得是纯 C++。
5.5 现在最推荐哪条线
如果是 C++ 项目,优先建议:
Node-APInode-addon-api@electron/rebuild
这是目前最稳的主线。
6. Native Addon 核心概念
根据 Node.js 官方文档,Addons 本质上是:
可以被 require() 当作普通 Node 模块加载的动态链接共享对象。
6.1 .node 文件是什么
它本质上是:
- Windows 下类似 DLL
- Linux 下类似
.so - macOS 下类似
.dylib/ Mach-O 动态库
但对 Node / Electron 来说,它统一表现为 .node 原生模块。
6.2 它和普通 JS 模块的区别
- 普通模块是 JS 文件
- Native Addon 是编译后的原生二进制
6.3 为什么 Native Addon 能被 require
因为 Node 在模块加载机制里支持加载原生扩展模块。
6.4 Addon 在 Electron 里为什么更复杂
因为 Electron 不是“你本机安装的那个 Node”,它自带自己的运行时组合,所以会引出:
- ABI 差异
- Electron headers
- 重编译
- 不同平台产物管理
7. Node-API、node-addon-api、NAN、直接 V8 的区别
这一节非常重要,也是面试高频。
7.1 Node-API
Node.js 官方提供的 C 风格 API。
官方文档明确写了两点:
- 它独立于底层 JavaScript 引擎
- 它是 ABI stable 的
也就是说它的目标是:
尽量把 Addon 从底层 V8 变化中隔离出来。
7.2 node-addon-api
这是 Node.js 官方组织维护的一个 C++ header-only 封装库。
可以把它理解成:
基于 Node-API 的 C++ 包装层
它的好处:
- 写法更像现代 C++
- 比直接写 C 风格 Node-API 更友好
- 更适合前端工程团队和混合栈团队上手
7.3 NAN
NAN 是早期常见的抽象层,主要用于屏蔽 V8 API 变化。
但现在如果做新项目,通常不作为第一推荐。
7.4 直接写 V8 / libuv / Node 内部 API
Node.js 官方文档明确给出三种 Addon 路线:
Node-APInan- 直接使用 V8、libuv、Node 内部库
但官方也明确建议:
除非需要直接访问 Node-API 未暴露的能力,否则优先使用 Node-API。
7.5 一句话结论
新项目优先 Node-API写 C++ 更推荐 node-addon-apiNAN 主要是历史项目会遇到直接 V8 路线最重,通常不建议作为默认选择
8. 为什么现在优先 Node-API
8.1 官方理由
Node.js 官方文档说明,Node-API 的目标是 ABI 稳定,并隔离底层 JavaScript 引擎变化。
这意味着:
- 升级 Node 的风险更低
- 维护成本更低
- 比直接绑定 V8 更稳
8.2 Electron 场景下的实际收益
Electron 经常升级 Chromium、Node 和运行时组合。
如果你的 Addon 强依赖 V8 细节,升级会更痛。
8.3 但这里要做一个工程判断
Node-API 的 ABI 稳定,不等于所有 Electron 场景都完全不用关心重建。
我根据官方资料做的务实判断是:
- Node-API 会显著降低版本升级脆弱性
- 但 Electron 官方仍明确强调 Native Module 在 Electron 中通常需要按 Electron 目标进行构建或重建
- 所以项目实践中仍应把
@electron/rebuild、预构建产物和平台架构校验纳入标准流程
这是一个基于官方文档的工程推断。
9. 最小可运行 Native Addon 示例
下面给一个偏学习型的最小示例,帮助你建立整体认知。
9.1 package.json 依赖思路
典型依赖会包括:
node-addon-apinode-gyp@electron/rebuild
9.2 binding.gyp 示例
{
"targets": [
{
"target_name": "native_hello",
"sources": ["src/native_hello.cc"],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"defines": ["NAPI_CPP_EXCEPTIONS"]
}
]
}
9.3 C++ 示例
#include <napi.h>
Napi::String Hello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "hello from c++");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("hello", Napi::Function::New(env, Hello));
return exports;
}
NODE_API_MODULE(native_hello, Init)
9.4 JS 调用
const nativeHello = require('./build/Release/native_hello.node')
console.log(nativeHello.hello())
9.5 这个例子想让你理解什么
不是为了背代码,而是理解:
- C++ 如何导出函数
- JS 如何像普通模块一样调用原生模块
- 中间靠的就是 Node-API / node-addon-api
10. Electron 中如何安全暴露 C++ 能力
10.1 推荐架构
推荐调用链:
Renderer -> Preload 白名单 API -> Main / native service -> Addon
10.2 为什么不推荐页面直接 require('.node')
因为这会让:
- 页面直接接触原生层
- 安全面变大
- 渲染层难测试
- 升级和替换困难
10.3 推荐封装方式
Preload
contextBridge.exposeInMainWorld('deviceApi', {
readDevice: () => ipcRenderer.invoke('device:read')
})
Main
ipcMain.handle('device:read', async () => {
return nativeDevice.read()
})
这样页面侧只知道:
window.deviceApi.readDevice()
而不需要知道底层是:
- C++
- 本地 DLL
- 还是其他实现
10.4 这样设计的好处
- 权限收口
- 接口稳定
- 更容易 mock
- 更容易做日志和错误监控
11. 异步任务、线程与性能问题
这是 Electron 调 C++ 最容易出线上问题的一节。
11.1 为什么不能在 Addon 里阻塞主线程
因为 Electron 的 Node 侧如果被长时间阻塞,会导致:
- UI 卡顿
- IPC 变慢
- 用户感觉应用“死掉了”
11.2 哪些操作要异步化
- 大文件处理
- 图像处理
- 编码解码
- AI 推理
- 硬件等待
- 网络或串口阻塞等待
11.3 常见异步思路
- libuv worker
- Node-API async work
- C++ 自己开线程后回调 JS
11.4 工程原则
- 不要阻塞事件循环
- 不要在错误线程直接操作 JS 对象
- 不要把复杂线程生命周期暴露给页面层
11.5 最重要的一句话
原生层再快,只要把 Electron 主线程堵住,用户体验就是慢。
12. 字符串、Buffer、对象、错误处理怎么传
12.1 字符串
最常见,注意:
- UTF-8
- 宽字符
- Windows 平台编码差异
12.2 二进制数据
音视频、图片、硬件协议经常用二进制。
这时重点是:
- Buffer 映射
- 内存所有权
- 拷贝成本
12.3 对象结构
复杂对象传递时建议:
- 扁平化
- 字段明确
- 避免过深嵌套
12.4 错误处理
最推荐的做法是统一错误模型:
- 错误码
- 错误消息
- 可选原生错误明细
不要把 C++ 层各种错误风格直接裸传给前端。
13. 构建链路:node-gyp、binding.gyp、@electron/rebuild
13.1 node-gyp 是什么
node-gyp 是 Node.js 官方组织维护的 Native Addon 构建工具。
它负责:
- 读取
binding.gyp - 下载 headers
- 生成平台构建文件
- 编译出
.node
13.2 binding.gyp 是什么
它可以理解成:
Native Addon 的构建描述文件
里面会定义:
- target name
- 源码文件
- include dirs
- 宏定义
- 链接库
13.3 为什么 Electron 里经常要重建
Electron 官方文档明确指出:
- Electron 与普通 Node 二进制 ABI 不同
- 原因之一是 Electron 使用了 Chromium 的 BoringSSL 而不是 Node 常见的 OpenSSL 组合
- 所以很多 Native Module 需要为 Electron 重新编译
13.4 @electron/rebuild 的作用
Electron 官方推荐使用 @electron/rebuild 来自动处理:
- Electron 版本识别
- headers 下载
- 本地模块重建
13.5 最常见工作流
- 安装依赖
- 编译 Addon
- Electron 升级后执行 rebuild
- 打包前再做一次目标环境校验
13.6 常见报错
报错 1:NODE_MODULE_VERSION 不匹配
说明:
编译目标和当前 Electron 运行时不匹配。
报错 2:Module did not self-register
常见原因:
- 编译 ABI 不对
- Windows delay-load hook 问题
报错 3:找不到符号 / procedure could not be found
常见原因:
- 链接错了运行时库
- 依赖 DLL 缺失
- Electron 目标版本不匹配
13.7 Windows 特别注意
Electron 官方文档特别强调,在 Windows 上:
win_delay_load_hook很重要- Electron 4+ 原生模块加载依赖 delay-load hook
这一点是 Windows 平台排查 Native Addon 问题的高频点。
14. Electron 打包发布时的 Native Addon 注意事项
开发环境能跑,不代表安装包能跑。
14.1 你需要考虑的维度
- 平台
- 架构
- Electron 版本
- 依赖动态库
- 打包工具配置
14.2 常见打包风险
.node文件没被打进去.dll/.so/.dylib漏拷贝- asar 导致原生模块无法正常加载
- 目标机器缺少运行时依赖
14.3 一般建议
- 原生模块通常不要直接塞进 asar 后裸加载
- 明确
asarUnpack - 对依赖动态库做平台目录管理
- 打包后用干净机器验证
14.4 更新流程注意点
当 Electron 自动更新后:
- 新版本 Electron 可能对应不同原生构建目标
- 原生模块必须和新安装包一起正确分发
不要把“JS 热更新”思维直接套到原生模块上。
15. 跨平台问题:Windows、macOS、Linux
15.1 Windows
重点:
- MSVC 工具链
win_delay_load_hook- DLL 依赖管理
- x64 / arm64 区分
15.2 macOS
重点:
- Xcode Command Line Tools
.dylib路径- 签名
- notarization 对原生依赖的影响
15.3 Linux
重点:
- gcc / clang 环境
- 系统动态库兼容
- 不同发行版依赖差异
15.4 跨平台开发的现实建议
- 不要指望一套原生产物通吃所有平台
- 每个平台单独构建和验证
- 尽量自动化 CI 构建
- 把平台相关逻辑封装在 native adapter 层
16. 实战架构建议
16.1 推荐目录结构
electron-app/
src/
main/
preload/
renderer/
native/
addon/
src/
binding.gyp
service/
deviceService.ts
16.2 推荐职责划分
native/addon
负责:
- 真正的 C++ 封装
- SDK 对接
- 数据结构转换
native/service
负责:
- JS 层二次封装
- 统一错误处理
- 统一日志与容错
main
负责:
- IPC 暴露
- 权限校验
- 生命周期管理
preload
负责:
- 暴露白名单 API
- 类型声明
renderer
只关心:
- 调用业务接口
- 展示结果
16.3 为什么这样分层
因为这样可以把最容易变化的层次隔离开:
- C++ 实现可能变
- SDK 可能换
- Electron 可能升级
- 页面 API 仍然尽量保持稳定
17. 高频面试题
17.1 Electron 为什么要调用 C++
答题模板:
Electron 调 C++ 主要是为了解决 JavaScript 不擅长或无法直接完成的问题,比如高性能计算、系统底层 API 访问、硬件设备通信,以及集成已有 C/C++ SDK。
17.2 Electron 调 C++ 有哪些方案
推荐回答:
- Native Node Addon
- FFI 调动态库
- 子进程调用本地程序
其中最常用、最稳定的通常是 Native Addon。
17.3 Node-API 和 NAN 有什么区别
推荐回答:
Node-API 是 Node.js 官方推荐的稳定 ABI 接口,目的是隔离底层引擎变化;NAN 更偏历史方案,主要用来屏蔽 V8 API 变化。新项目一般优先 Node-API。
17.4 node-addon-api 是什么
它是基于 Node-API 的 C++ header-only 封装层,让你能用更现代的 C++ 方式写 Addon。
17.5 为什么 Electron 原生模块经常要 rebuild
推荐回答:
因为 Electron 和普通 Node.js 二进制存在 ABI 差异,所以很多原生模块需要针对 Electron 目标版本重新编译。官方一般推荐使用 @electron/rebuild。
17.6 为什么不建议在 Renderer 直接调用 .node
因为会破坏安全边界和架构分层。更推荐通过 Preload 和 IPC 暴露白名单化接口,把原生能力收口在主进程或专门的 native service 层。
17.7 Native Addon 最大的工程难点是什么
- ABI 和重编译
- 跨平台构建
- 打包发布
- 内存管理
- 线程与异步回调
17.8 如何避免原生层导致 UI 卡顿
- 不在主线程做耗时操作
- 使用异步工作线程
- 明确 JS 线程和原生线程边界
17.9 Electron + C++ 项目如何做工程化
答题方向:
- Node-API / node-addon-api
node-gyp@electron/rebuild- 平台产物管理
- 打包时处理
asarUnpack - CI 为多平台产出二进制
18. 学习路线建议
18.1 第一阶段:先学 Electron 架构
先搞清楚:
- Main
- Renderer
- Preload
- IPC
18.2 第二阶段:补 C++ 基础
重点补:
- 指针和引用
- 类与对象
- 内存管理
- STL
- 线程
18.3 第三阶段:上手 Node-API / node-addon-api
先从最小 hello world 开始,再做:
- 参数传递
- Buffer
- 类封装
- 异步任务
18.4 第四阶段:接第三方 SDK
建议挑一个真实场景练习:
- 图像处理库
- 设备 SDK
- 本地加解密库
18.5 第五阶段:做打包发布
这一阶段一定要实操:
- rebuild
- asar 配置
- 多平台构建
- 干净环境验证
19. 官方资料入口
- Electron Native Node Modules
- Electron Native Code and Electron
- Electron contextBridge
- Electron Process Model
- Node.js C++ addons
- Node.js Node-API
- node-addon-api
- node-gyp
- node-addon-examples
20. 一页速记总结
20.1 结论先记住
Electron 调 C++ 最主流的是 Native Addon新项目优先 Node-API / node-addon-api页面不要直接乱碰原生模块Electron 升级要重点关注 rebuild真正难点不在 hello world,而在跨平台和发布
20.2 推荐选型
新项目
node-addon-apinode-gyp@electron/rebuild
历史项目
- 可能会遇到
NAN - 甚至直接 V8 / libuv
20.3 记忆口诀
前端管界面,Preload 做桥,主进程收口,Node-API 接原生,rebuild 保兼容,异步线程保不卡。
21. 背诵口诀
能不用原生就别滥用,要用就走标准链路;新项目优先 Node-API,Electron 升级记得 rebuild;页面层只拿白名单,重活别堵主线程。