文章目录

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。


目录

  1. 学习总览
  2. Electron 为什么要调用 C++
  3. Electron 调 C++ 的完整调用链
  4. 做这件事前必须掌握的 C++ 基础
  5. Electron 中调用 C++ 的几种方案
  6. Native Addon 核心概念
  7. Node-API、node-addon-api、NAN、直接 V8 的区别
  8. 为什么现在优先 Node-API
  9. 最小可运行 Native Addon 示例
  10. Electron 中如何安全暴露 C++ 能力
  11. 异步任务、线程与性能问题
  12. 字符串、Buffer、对象、错误处理怎么传
  13. 构建链路:node-gyp、binding.gyp、@electron/rebuild
  14. Electron 打包发布时的 Native Addon 注意事项
  15. 跨平台问题:Windows、macOS、Linux
  16. 实战架构建议
  17. 高频面试题
  18. 学习路线建议
  19. 官方资料入口
  20. 一页速记总结
  21. 背诵口诀

1. 学习总览

1.1 这部分到底在学什么

很多前端同学一看到 Electron 调 C++,容易把它理解成:

  1. 写个 .node 文件
  2. 在 Electron 里 require
  3. 调一下方法

但真实项目里,这件事远不止这么简单。

你真正学的是:

如何把 Electron 的 JavaScript 世界、Node 原生模块世界和 C++ / 系统能力世界稳定地连接起来。

这里面会涉及:

  1. C++ 基础类型和内存模型
  2. Node Native Addon 机制
  3. Electron 的 ABI 与重编译
  4. Preload / IPC 的安全边界
  5. 异步线程与事件循环
  6. 跨平台编译和打包发布

1.2 学习主线

主线 1:为什么要调用 C++

你要先知道需求来自哪里,而不是为了“炫技”上原生。

主线 2:Addons 怎么嵌进 Electron

Native Addon 如何被 Node / Electron 加载,ABI 为什么会出问题。

主线 3:为什么现在优先 Node-API

这关系到稳定性、升级成本和维护成本。

主线 4:工程上怎么落地

开发环境能跑不代表生产能发,真正难点常常在:

  1. 编译
  2. 重建
  3. 打包
  4. 签名
  5. 跨平台

2. Electron 为什么要调用 C++

2.1 常见原因

Electron 本身已经能做很多事情,但仍然有场景必须或非常适合接 C++:

  1. 调系统底层 API
  2. 接已有 C / C++ SDK
  3. 性能敏感计算
  4. 音视频编解码
  5. 图像处理
  6. 硬件设备通信
  7. 驱动层或厂商库接入

2.2 典型业务场景

  1. 摄像头、麦克风、串口、USB 设备控制
  2. 调工业设备、扫码枪、打印机 SDK
  3. OCR、音视频处理、AI 推理引擎接入
  4. 高性能本地加密、压缩、特征计算
  5. 和公司已有 C++ 代码库集成

2.3 为什么不直接全用 JavaScript

因为 JavaScript 并不总能解决:

  1. 底层系统 API 可达性
  2. 已有原生 SDK 集成
  3. 计算密集型性能
  4. 与操作系统或硬件的紧耦合能力

2.4 但为什么也不能滥用 C++

因为原生接入会增加:

  1. 开发难度
  2. 跨平台复杂度
  3. 调试成本
  4. 发布成本
  5. 升级成本

一句话:

Electron 调 C++ 是为了解决 JavaScript 不擅长的问题,不是为了把所有逻辑都搬到原生层。


3. Electron 调 C++ 的完整调用链

最推荐理解的完整链路是:

Renderer UI -> Preload API -> ipcRenderer / 直接 Node 接口 -> Main / Node Runtime -> Native Addon -> C++ / SDK / OS API -> 返回结果

3.1 最常见工程分层

Renderer

只负责:

  1. UI
  2. 用户交互
  3. 展示结果

Preload

负责:

  1. 向页面暴露白名单 API
  2. 屏蔽底层实现细节
  3. 限制页面直接拿到 Node 能力

Main 或 Node 层

负责:

  1. 调用 Native Addon
  2. 编排业务流程
  3. 管理权限和资源

C++ Addon

负责:

  1. 封装原生 SDK
  2. 转换数据结构
  3. 执行高性能逻辑

3.2 为什么不建议 Renderer 直接乱调 Addon

因为这会导致:

  1. 安全边界弱
  2. 页面层和底层强耦合
  3. 测试困难
  4. 升级困难

更好的思路是:

把 Native Addon 看成基础设施能力,由主进程或专门的 native service 层统一收口。


4. 做这件事前必须掌握的 C++ 基础

如果你的目标是 Electron 调 C++ 开发,C++ 不需要一上来就学到模板元编程,但下面这些基础必须够稳。

4.1 语法基础

  1. 变量、函数、类
  2. 引用、指针
  3. 构造函数、析构函数
  4. 命名空间
  5. const
  6. 头文件与实现文件分离

4.2 内存与对象生命周期

这是最重要的一块。

必须掌握:

  1. 栈和堆
  2. new / delete
  3. RAII
  4. 智能指针
  5. 对象何时创建、何时销毁

因为 Electron + Native Addon 里最容易踩坑的往往就是:

  1. 内存泄漏
  2. 悬空指针
  3. 重复释放
  4. 线程竞争导致的对象生命周期异常

4.3 STL 常用容器

至少要熟:

  1. std::string
  2. std::vector
  3. std::map / std::unordered_map
  4. std::optional
  5. std::unique_ptr
  6. std::shared_ptr

4.4 错误处理

要理解:

  1. 返回码风格
  2. 异常风格
  3. C 接口和 C++ 接口的差异

很多第三方 SDK 不是抛异常,而是返回错误码。

4.5 并发与线程

必须理解:

  1. 线程和主线程
  2. 互斥锁
  3. 条件变量
  4. 线程安全
  5. 不要阻塞 Node 主线程

4.6 C++ 对 Electron 开发最重要的能力画像

如果你的目标不是做纯 C++ 工程师,而是做 Electron 原生集成,那么最重要的不是刷复杂语法,而是:

  1. 能读懂 SDK 头文件
  2. 能写对象封装
  3. 能做内存管理
  4. 能处理线程与异步
  5. 能把 C++ 数据映射回 JS

5. Electron 中调用 C++ 的几种方案

5.1 Node Native Addon

这是最主流、最标准的方式。

特点:

  1. 直接在 Node / Electron 运行时里加载 .node 模块
  2. 接口像普通 JS 模块一样调用
  3. 性能好
  4. 与 Electron 集成最自然

5.2 调动态库

比如通过:

  1. ffi-napi
  2. 自定义桥接层

调用 .dll.so.dylib

优点:

  1. 可以直接复用已有库

缺点:

  1. 类型映射复杂
  2. 调试难
  3. 稳定性与维护性可能不如 Addon 封装

5.3 子进程调用本地可执行程序

通过:

  1. child_process.spawn
  2. execFile

调用本地 exe 或命令行程序。

适合:

  1. 已有独立 CLI 工具
  2. 进程隔离更重要

缺点:

  1. 通信成本更高
  2. 启动耗时更高
  3. 数据交互不够自然

5.4 Rust / C 包装后再接入

虽然你问的是 C++,但工程上也常见:

  1. Rust 编译成 Node Addon
  2. C 库封装成 Addon

这说明:

Electron 调原生的本质是 Native Addon,不一定非得是纯 C++。

5.5 现在最推荐哪条线

如果是 C++ 项目,优先建议:

  1. Node-API
  2. node-addon-api
  3. @electron/rebuild

这是目前最稳的主线。


6. Native Addon 核心概念

根据 Node.js 官方文档,Addons 本质上是:

可以被 require() 当作普通 Node 模块加载的动态链接共享对象。

6.1 .node 文件是什么

它本质上是:

  1. Windows 下类似 DLL
  2. Linux 下类似 .so
  3. macOS 下类似 .dylib / Mach-O 动态库

但对 Node / Electron 来说,它统一表现为 .node 原生模块。

6.2 它和普通 JS 模块的区别

  1. 普通模块是 JS 文件
  2. Native Addon 是编译后的原生二进制

6.3 为什么 Native Addon 能被 require

因为 Node 在模块加载机制里支持加载原生扩展模块。

6.4 Addon 在 Electron 里为什么更复杂

因为 Electron 不是“你本机安装的那个 Node”,它自带自己的运行时组合,所以会引出:

  1. ABI 差异
  2. Electron headers
  3. 重编译
  4. 不同平台产物管理

7. Node-API、node-addon-api、NAN、直接 V8 的区别

这一节非常重要,也是面试高频。

7.1 Node-API

Node.js 官方提供的 C 风格 API。
官方文档明确写了两点:

  1. 它独立于底层 JavaScript 引擎
  2. 它是 ABI stable 的

也就是说它的目标是:

尽量把 Addon 从底层 V8 变化中隔离出来。

7.2 node-addon-api

这是 Node.js 官方组织维护的一个 C++ header-only 封装库。
可以把它理解成:

基于 Node-API 的 C++ 包装层

它的好处:

  1. 写法更像现代 C++
  2. 比直接写 C 风格 Node-API 更友好
  3. 更适合前端工程团队和混合栈团队上手

7.3 NAN

NAN 是早期常见的抽象层,主要用于屏蔽 V8 API 变化。
但现在如果做新项目,通常不作为第一推荐。

7.4 直接写 V8 / libuv / Node 内部 API

Node.js 官方文档明确给出三种 Addon 路线:

  1. Node-API
  2. nan
  3. 直接使用 V8、libuv、Node 内部库

但官方也明确建议:

除非需要直接访问 Node-API 未暴露的能力,否则优先使用 Node-API。

7.5 一句话结论

  1. 新项目优先 Node-API
  2. 写 C++ 更推荐 node-addon-api
  3. NAN 主要是历史项目会遇到
  4. 直接 V8 路线最重,通常不建议作为默认选择

8. 为什么现在优先 Node-API

8.1 官方理由

Node.js 官方文档说明,Node-API 的目标是 ABI 稳定,并隔离底层 JavaScript 引擎变化。

这意味着:

  1. 升级 Node 的风险更低
  2. 维护成本更低
  3. 比直接绑定 V8 更稳

8.2 Electron 场景下的实际收益

Electron 经常升级 Chromium、Node 和运行时组合。
如果你的 Addon 强依赖 V8 细节,升级会更痛。

8.3 但这里要做一个工程判断

Node-API 的 ABI 稳定,不等于所有 Electron 场景都完全不用关心重建。
我根据官方资料做的务实判断是:

  1. Node-API 会显著降低版本升级脆弱性
  2. 但 Electron 官方仍明确强调 Native Module 在 Electron 中通常需要按 Electron 目标进行构建或重建
  3. 所以项目实践中仍应把 @electron/rebuild、预构建产物和平台架构校验纳入标准流程

这是一个基于官方文档的工程推断。


9. 最小可运行 Native Addon 示例

下面给一个偏学习型的最小示例,帮助你建立整体认知。

9.1 package.json 依赖思路

典型依赖会包括:

  1. node-addon-api
  2. node-gyp
  3. @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 这个例子想让你理解什么

不是为了背代码,而是理解:

  1. C++ 如何导出函数
  2. JS 如何像普通模块一样调用原生模块
  3. 中间靠的就是 Node-API / node-addon-api

10. Electron 中如何安全暴露 C++ 能力

10.1 推荐架构

推荐调用链:

Renderer -> Preload 白名单 API -> Main / native service -> Addon

10.2 为什么不推荐页面直接 require('.node')

因为这会让:

  1. 页面直接接触原生层
  2. 安全面变大
  3. 渲染层难测试
  4. 升级和替换困难

10.3 推荐封装方式

Preload

contextBridge.exposeInMainWorld('deviceApi', {
  readDevice: () => ipcRenderer.invoke('device:read')
})

Main

ipcMain.handle('device:read', async () => {
  return nativeDevice.read()
})

这样页面侧只知道:

window.deviceApi.readDevice()

而不需要知道底层是:

  1. C++
  2. 本地 DLL
  3. 还是其他实现

10.4 这样设计的好处

  1. 权限收口
  2. 接口稳定
  3. 更容易 mock
  4. 更容易做日志和错误监控

11. 异步任务、线程与性能问题

这是 Electron 调 C++ 最容易出线上问题的一节。

11.1 为什么不能在 Addon 里阻塞主线程

因为 Electron 的 Node 侧如果被长时间阻塞,会导致:

  1. UI 卡顿
  2. IPC 变慢
  3. 用户感觉应用“死掉了”

11.2 哪些操作要异步化

  1. 大文件处理
  2. 图像处理
  3. 编码解码
  4. AI 推理
  5. 硬件等待
  6. 网络或串口阻塞等待

11.3 常见异步思路

  1. libuv worker
  2. Node-API async work
  3. C++ 自己开线程后回调 JS

11.4 工程原则

  1. 不要阻塞事件循环
  2. 不要在错误线程直接操作 JS 对象
  3. 不要把复杂线程生命周期暴露给页面层

11.5 最重要的一句话

原生层再快,只要把 Electron 主线程堵住,用户体验就是慢。


12. 字符串、Buffer、对象、错误处理怎么传

12.1 字符串

最常见,注意:

  1. UTF-8
  2. 宽字符
  3. Windows 平台编码差异

12.2 二进制数据

音视频、图片、硬件协议经常用二进制。
这时重点是:

  1. Buffer 映射
  2. 内存所有权
  3. 拷贝成本

12.3 对象结构

复杂对象传递时建议:

  1. 扁平化
  2. 字段明确
  3. 避免过深嵌套

12.4 错误处理

最推荐的做法是统一错误模型:

  1. 错误码
  2. 错误消息
  3. 可选原生错误明细

不要把 C++ 层各种错误风格直接裸传给前端。


13. 构建链路:node-gyp、binding.gyp、@electron/rebuild

13.1 node-gyp 是什么

node-gyp 是 Node.js 官方组织维护的 Native Addon 构建工具。
它负责:

  1. 读取 binding.gyp
  2. 下载 headers
  3. 生成平台构建文件
  4. 编译出 .node

13.2 binding.gyp 是什么

它可以理解成:

Native Addon 的构建描述文件

里面会定义:

  1. target name
  2. 源码文件
  3. include dirs
  4. 宏定义
  5. 链接库

13.3 为什么 Electron 里经常要重建

Electron 官方文档明确指出:

  1. Electron 与普通 Node 二进制 ABI 不同
  2. 原因之一是 Electron 使用了 Chromium 的 BoringSSL 而不是 Node 常见的 OpenSSL 组合
  3. 所以很多 Native Module 需要为 Electron 重新编译

13.4 @electron/rebuild 的作用

Electron 官方推荐使用 @electron/rebuild 来自动处理:

  1. Electron 版本识别
  2. headers 下载
  3. 本地模块重建

13.5 最常见工作流

  1. 安装依赖
  2. 编译 Addon
  3. Electron 升级后执行 rebuild
  4. 打包前再做一次目标环境校验

13.6 常见报错

报错 1:NODE_MODULE_VERSION 不匹配

说明:

编译目标和当前 Electron 运行时不匹配。

报错 2:Module did not self-register

常见原因:

  1. 编译 ABI 不对
  2. Windows delay-load hook 问题

报错 3:找不到符号 / procedure could not be found

常见原因:

  1. 链接错了运行时库
  2. 依赖 DLL 缺失
  3. Electron 目标版本不匹配

13.7 Windows 特别注意

Electron 官方文档特别强调,在 Windows 上:

  1. win_delay_load_hook 很重要
  2. Electron 4+ 原生模块加载依赖 delay-load hook

这一点是 Windows 平台排查 Native Addon 问题的高频点。


14. Electron 打包发布时的 Native Addon 注意事项

开发环境能跑,不代表安装包能跑。

14.1 你需要考虑的维度

  1. 平台
  2. 架构
  3. Electron 版本
  4. 依赖动态库
  5. 打包工具配置

14.2 常见打包风险

  1. .node 文件没被打进去
  2. .dll / .so / .dylib 漏拷贝
  3. asar 导致原生模块无法正常加载
  4. 目标机器缺少运行时依赖

14.3 一般建议

  1. 原生模块通常不要直接塞进 asar 后裸加载
  2. 明确 asarUnpack
  3. 对依赖动态库做平台目录管理
  4. 打包后用干净机器验证

14.4 更新流程注意点

当 Electron 自动更新后:

  1. 新版本 Electron 可能对应不同原生构建目标
  2. 原生模块必须和新安装包一起正确分发

不要把“JS 热更新”思维直接套到原生模块上。


15. 跨平台问题:Windows、macOS、Linux

15.1 Windows

重点:

  1. MSVC 工具链
  2. win_delay_load_hook
  3. DLL 依赖管理
  4. x64 / arm64 区分

15.2 macOS

重点:

  1. Xcode Command Line Tools
  2. .dylib 路径
  3. 签名
  4. notarization 对原生依赖的影响

15.3 Linux

重点:

  1. gcc / clang 环境
  2. 系统动态库兼容
  3. 不同发行版依赖差异

15.4 跨平台开发的现实建议

  1. 不要指望一套原生产物通吃所有平台
  2. 每个平台单独构建和验证
  3. 尽量自动化 CI 构建
  4. 把平台相关逻辑封装在 native adapter 层

16. 实战架构建议

16.1 推荐目录结构

electron-app/
  src/
    main/
    preload/
    renderer/
    native/
      addon/
        src/
        binding.gyp
      service/
        deviceService.ts

16.2 推荐职责划分

native/addon

负责:

  1. 真正的 C++ 封装
  2. SDK 对接
  3. 数据结构转换

native/service

负责:

  1. JS 层二次封装
  2. 统一错误处理
  3. 统一日志与容错

main

负责:

  1. IPC 暴露
  2. 权限校验
  3. 生命周期管理

preload

负责:

  1. 暴露白名单 API
  2. 类型声明

renderer

只关心:

  1. 调用业务接口
  2. 展示结果

16.3 为什么这样分层

因为这样可以把最容易变化的层次隔离开:

  1. C++ 实现可能变
  2. SDK 可能换
  3. Electron 可能升级
  4. 页面 API 仍然尽量保持稳定

17. 高频面试题

17.1 Electron 为什么要调用 C++

答题模板:

Electron 调 C++ 主要是为了解决 JavaScript 不擅长或无法直接完成的问题,比如高性能计算、系统底层 API 访问、硬件设备通信,以及集成已有 C/C++ SDK。

17.2 Electron 调 C++ 有哪些方案

推荐回答:

  1. Native Node Addon
  2. FFI 调动态库
  3. 子进程调用本地程序

其中最常用、最稳定的通常是 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 最大的工程难点是什么

  1. ABI 和重编译
  2. 跨平台构建
  3. 打包发布
  4. 内存管理
  5. 线程与异步回调

17.8 如何避免原生层导致 UI 卡顿

  1. 不在主线程做耗时操作
  2. 使用异步工作线程
  3. 明确 JS 线程和原生线程边界

17.9 Electron + C++ 项目如何做工程化

答题方向:

  1. Node-API / node-addon-api
  2. node-gyp
  3. @electron/rebuild
  4. 平台产物管理
  5. 打包时处理 asarUnpack
  6. CI 为多平台产出二进制

18. 学习路线建议

18.1 第一阶段:先学 Electron 架构

先搞清楚:

  1. Main
  2. Renderer
  3. Preload
  4. IPC

18.2 第二阶段:补 C++ 基础

重点补:

  1. 指针和引用
  2. 类与对象
  3. 内存管理
  4. STL
  5. 线程

18.3 第三阶段:上手 Node-API / node-addon-api

先从最小 hello world 开始,再做:

  1. 参数传递
  2. Buffer
  3. 类封装
  4. 异步任务

18.4 第四阶段:接第三方 SDK

建议挑一个真实场景练习:

  1. 图像处理库
  2. 设备 SDK
  3. 本地加解密库

18.5 第五阶段:做打包发布

这一阶段一定要实操:

  1. rebuild
  2. asar 配置
  3. 多平台构建
  4. 干净环境验证

19. 官方资料入口

  1. Electron Native Node Modules
  2. Electron Native Code and Electron
  3. Electron contextBridge
  4. Electron Process Model
  5. Node.js C++ addons
  6. Node.js Node-API
  7. node-addon-api
  8. node-gyp
  9. node-addon-examples

20. 一页速记总结

20.1 结论先记住

  1. Electron 调 C++ 最主流的是 Native Addon
  2. 新项目优先 Node-API / node-addon-api
  3. 页面不要直接乱碰原生模块
  4. Electron 升级要重点关注 rebuild
  5. 真正难点不在 hello world,而在跨平台和发布

20.2 推荐选型

新项目

  1. node-addon-api
  2. node-gyp
  3. @electron/rebuild

历史项目

  1. 可能会遇到 NAN
  2. 甚至直接 V8 / libuv

20.3 记忆口诀

前端管界面,Preload 做桥,主进程收口,Node-API 接原生,rebuild 保兼容,异步线程保不卡。


21. 背诵口诀

能不用原生就别滥用,要用就走标准链路;新项目优先 Node-API,Electron 升级记得 rebuild;页面层只拿白名单,重活别堵主线程。