Electron 与 Native Addon 实战模板笔记
适合目标:把 Electron 调 C++ 的知识真正落到项目结构、开发脚手架、构建流程和发布方案上。
学习重点:最小项目结构、Preload/IPC/Native Service 分层、node-addon-api 示例、rebuild 流程、打包发布和排错。
学习原则:先搭出可运行最小闭环,再逐步接入真实 SDK,不要一开始把原生层和业务层揉在一起。
目录
- 学习总览
- 推荐项目结构
- 最小调用链模板
- package.json 与依赖建议
- binding.gyp 模板
- C++ Addon 模板
- Main / Preload / Renderer 模板
- Native Service 层怎么写
- 调试与日志建议
- 构建与重建流程
- 打包发布建议
- 常见问题排查
- 面试考点
- 一页速记总结
1. 学习总览
1.1 这篇文档解决什么问题
前一篇更偏知识体系,这一篇更偏落地:
- 项目该怎么分层
- 模板代码怎么写
- 原生模块怎么接到 Electron
- 本地开发、重建、发布怎么串起来
1.2 实战最容易踩的坑
- 页面直接依赖
.node - 原生层和业务逻辑耦合
- Electron 升级后 Native Addon 失效
- 打包后
.node找不到 - 平台差异没人管
2. 推荐项目结构
electron-native-app/
src/
main/
index.ts
ipc/
nativeIpc.ts
preload/
index.ts
renderer/
app.tsx
native/
addon/
binding.gyp
src/
hello.cc
service/
nativeBridge.ts
deviceService.ts
package.json
2.1 每层职责
src/native/addon
- C++ 实现
- 原生 SDK 封装
src/native/service
- JS 层统一封装
- 统一错误处理
- 统一日志
src/main/ipc
- IPC 注册
- 权限收口
src/preload
- 暴露白名单 API
src/renderer
- UI
- 只调用
window.xxxApi
3. 最小调用链模板
推荐完整链路:
Renderer -> Preload -> ipcRenderer.invoke -> Main ipcMain.handle -> native service -> .node Addon
这是最稳的默认方案。
3.1 为什么不直接 Renderer -> Addon
因为会带来:
- 安全面扩大
- 页面和原生强耦合
- 测试困难
- 发布变更风险更大
4. package.json 与依赖建议
典型依赖思路:
{
"devDependencies": {
"@electron/rebuild": "^latest",
"node-addon-api": "^latest",
"node-gyp": "^latest"
}
}
4.1 为什么这三个基本是标配
node-addon-api:用 C++ 方式写 Node-APInode-gyp:构建 Addon@electron/rebuild:Electron 升级后重建原生模块
5. binding.gyp 模板
{
"targets": [
{
"target_name": "native_device",
"sources": ["src/hello.cc"],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"defines": ["NAPI_CPP_EXCEPTIONS"]
}
]
}
5.1 如果要接第三方 SDK
通常还会补:
librarieslibrary_dirsinclude_dirs- 平台相关宏
6. C++ Addon 模板
6.1 最小 hello 示例
#include <napi.h>
Napi::String Hello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "hello from native addon");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("hello", Napi::Function::New(env, Hello));
return exports;
}
NODE_API_MODULE(native_device, Init)
6.2 面向设备类封装的思路
如果是 SDK 接入,不建议全部暴露成零散函数。
更推荐:
- 原生层先封成类
- Addon 暴露必要方法
- JS 层再二次包装
7. Main / Preload / Renderer 模板
7.1 Native Service 层
// src/native/service/nativeBridge.ts
const nativeAddon = require('../../../build/Release/native_device.node')
export function readNativeHello() {
return nativeAddon.hello()
}
7.2 Main IPC
// src/main/ipc/nativeIpc.ts
import { ipcMain } from 'electron'
import { readNativeHello } from '../../native/service/nativeBridge'
export function registerNativeIpc() {
ipcMain.handle('native:hello', () => {
return readNativeHello()
})
}
7.3 Preload
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('nativeApi', {
hello: () => ipcRenderer.invoke('native:hello')
})
7.4 Renderer
const result = await window.nativeApi.hello()
console.log(result)
7.5 这样做的价值
- UI 层不感知原生模块路径
- 可统一做权限控制
- 可统一做监控和容错
8. Native Service 层怎么写
这是很多项目忽略但非常关键的一层。
8.1 这一层做什么
- 包装原生方法
- 做参数校验
- 处理错误码
- 做结果转换
- 做日志记录
8.2 为什么不要直接把 Addon 原样透出
因为原生方法往往:
- 命名不友好
- 错误风格不统一
- 参数风格不统一
- 可能暴露太多底层细节
8.3 推荐封装示例
export async function connectDevice() {
try {
const result = nativeAddon.connect()
return { ok: true, data: result }
} catch (error) {
return {
ok: false,
message: error instanceof Error ? error.message : 'native connect failed'
}
}
}
9. 调试与日志建议
9.1 开发期
建议至少区分三层日志:
- Renderer 日志
- Main / IPC 日志
- Native 层日志
9.2 为什么一定要分层
因为 Electron + Native 出问题时,问题可能出在:
- 页面没调用到
- IPC 没注册成功
- Addon 没加载
- C++ 内部崩了
9.3 Native 层调试要点
- 输出关键参数
- 记录 SDK 错误码
- 记录线程切换点
- 记录动态库加载状态
10. 构建与重建流程
10.1 开发流程建议
- 安装依赖
node-gyp rebuild- 启动 Electron
- 若 Electron 版本变化,执行
@electron/rebuild
10.2 为什么 @electron/rebuild 很关键
因为 Electron 目标运行时和普通 Node 不一样,原生模块经常需要按 Electron 版本重建。
10.3 推荐脚本思路
{
"scripts": {
"native:build": "node-gyp rebuild",
"native:rebuild:electron": "electron-rebuild",
"dev": "npm run native:rebuild:electron && electron ."
}
}
11. 打包发布建议
11.1 核心原则
.node文件要带进安装包- 动态库依赖不能漏
- 注意
asarUnpack - 按平台分别验证
11.2 实战建议
- 不要只在开发机验证
- 用干净机器跑安装包
- 验证 x64 / arm64
- 验证升级场景
11.3 常见漏项
.dll没打包.dylib路径不对.so在目标机缺依赖- asar 把原生模块包坏了
12. 常见问题排查
12.1 NODE_MODULE_VERSION 不匹配
一般就是:
- Node 版本不对
- Electron 版本不对
- 没 rebuild
12.2 Module did not self-register
常见方向:
- ABI 不匹配
- 构建目标不对
- Windows delay-load 相关问题
12.3 Cannot find module *.node
常见方向:
- 路径写死
- 打包时没带进去
- asar 配置不对
12.4 页面调用没反应
检查顺序:
- Renderer 调到了吗
- Preload 暴露了吗
- IPC 注册了吗
- Main 拿到 Addon 了吗
- Addon 内部报错了吗
13. 面试考点
13.1 Electron 调 Native Addon 的推荐调用链是什么
推荐回答:
Renderer 通过 Preload 暴露的白名单 API 调用 IPC,主进程再统一调用 Native Service 和 Addon,这样能兼顾安全性、可维护性和架构解耦。
13.2 为什么要加 Native Service 层
因为它能收口原生接口、统一错误处理、避免页面层直接依赖原生实现细节。
13.3 为什么开发能跑、打包后不一定能跑
因为打包后涉及:
.node文件路径- 动态库分发
- asar
- 目标机依赖环境
14. 一页速记总结
14.1 实战模板主线
Addon负责原生封装Native Service负责 JS 层整理Main IPC负责权限与调度Preload负责白名单暴露Renderer只负责业务调用
14.2 记忆口诀
原生层做薄,服务层收口,Main 做调度,Preload 做白名单,Renderer 不碰底层。