文章目录

Electron 与 Native Addon 实战模板笔记

适合目标:把 Electron 调 C++ 的知识真正落到项目结构、开发脚手架、构建流程和发布方案上。
学习重点:最小项目结构、Preload/IPC/Native Service 分层、node-addon-api 示例、rebuild 流程、打包发布和排错。
学习原则:先搭出可运行最小闭环,再逐步接入真实 SDK,不要一开始把原生层和业务层揉在一起。


目录

  1. 学习总览
  2. 推荐项目结构
  3. 最小调用链模板
  4. package.json 与依赖建议
  5. binding.gyp 模板
  6. C++ Addon 模板
  7. Main / Preload / Renderer 模板
  8. Native Service 层怎么写
  9. 调试与日志建议
  10. 构建与重建流程
  11. 打包发布建议
  12. 常见问题排查
  13. 面试考点
  14. 一页速记总结

1. 学习总览

1.1 这篇文档解决什么问题

前一篇更偏知识体系,这一篇更偏落地:

  1. 项目该怎么分层
  2. 模板代码怎么写
  3. 原生模块怎么接到 Electron
  4. 本地开发、重建、发布怎么串起来

1.2 实战最容易踩的坑

  1. 页面直接依赖 .node
  2. 原生层和业务逻辑耦合
  3. Electron 升级后 Native Addon 失效
  4. 打包后 .node 找不到
  5. 平台差异没人管

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

  1. C++ 实现
  2. 原生 SDK 封装

src/native/service

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

src/main/ipc

  1. IPC 注册
  2. 权限收口

src/preload

  1. 暴露白名单 API

src/renderer

  1. UI
  2. 只调用 window.xxxApi

3. 最小调用链模板

推荐完整链路:

Renderer -> Preload -> ipcRenderer.invoke -> Main ipcMain.handle -> native service -> .node Addon

这是最稳的默认方案。

3.1 为什么不直接 Renderer -> Addon

因为会带来:

  1. 安全面扩大
  2. 页面和原生强耦合
  3. 测试困难
  4. 发布变更风险更大

4. package.json 与依赖建议

典型依赖思路:

{
  "devDependencies": {
    "@electron/rebuild": "^latest",
    "node-addon-api": "^latest",
    "node-gyp": "^latest"
  }
}

4.1 为什么这三个基本是标配

  1. node-addon-api:用 C++ 方式写 Node-API
  2. node-gyp:构建 Addon
  3. @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

通常还会补:

  1. libraries
  2. library_dirs
  3. include_dirs
  4. 平台相关宏

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 接入,不建议全部暴露成零散函数。
更推荐:

  1. 原生层先封成类
  2. Addon 暴露必要方法
  3. 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 这样做的价值

  1. UI 层不感知原生模块路径
  2. 可统一做权限控制
  3. 可统一做监控和容错

8. Native Service 层怎么写

这是很多项目忽略但非常关键的一层。

8.1 这一层做什么

  1. 包装原生方法
  2. 做参数校验
  3. 处理错误码
  4. 做结果转换
  5. 做日志记录

8.2 为什么不要直接把 Addon 原样透出

因为原生方法往往:

  1. 命名不友好
  2. 错误风格不统一
  3. 参数风格不统一
  4. 可能暴露太多底层细节

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 开发期

建议至少区分三层日志:

  1. Renderer 日志
  2. Main / IPC 日志
  3. Native 层日志

9.2 为什么一定要分层

因为 Electron + Native 出问题时,问题可能出在:

  1. 页面没调用到
  2. IPC 没注册成功
  3. Addon 没加载
  4. C++ 内部崩了

9.3 Native 层调试要点

  1. 输出关键参数
  2. 记录 SDK 错误码
  3. 记录线程切换点
  4. 记录动态库加载状态

10. 构建与重建流程

10.1 开发流程建议

  1. 安装依赖
  2. node-gyp rebuild
  3. 启动 Electron
  4. 若 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 核心原则

  1. .node 文件要带进安装包
  2. 动态库依赖不能漏
  3. 注意 asarUnpack
  4. 按平台分别验证

11.2 实战建议

  1. 不要只在开发机验证
  2. 用干净机器跑安装包
  3. 验证 x64 / arm64
  4. 验证升级场景

11.3 常见漏项

  1. .dll 没打包
  2. .dylib 路径不对
  3. .so 在目标机缺依赖
  4. asar 把原生模块包坏了

12. 常见问题排查

12.1 NODE_MODULE_VERSION 不匹配

一般就是:

  1. Node 版本不对
  2. Electron 版本不对
  3. 没 rebuild

12.2 Module did not self-register

常见方向:

  1. ABI 不匹配
  2. 构建目标不对
  3. Windows delay-load 相关问题

12.3 Cannot find module *.node

常见方向:

  1. 路径写死
  2. 打包时没带进去
  3. asar 配置不对

12.4 页面调用没反应

检查顺序:

  1. Renderer 调到了吗
  2. Preload 暴露了吗
  3. IPC 注册了吗
  4. Main 拿到 Addon 了吗
  5. Addon 内部报错了吗

13. 面试考点

13.1 Electron 调 Native Addon 的推荐调用链是什么

推荐回答:

Renderer 通过 Preload 暴露的白名单 API 调用 IPC,主进程再统一调用 Native Service 和 Addon,这样能兼顾安全性、可维护性和架构解耦。

13.2 为什么要加 Native Service 层

因为它能收口原生接口、统一错误处理、避免页面层直接依赖原生实现细节。

13.3 为什么开发能跑、打包后不一定能跑

因为打包后涉及:

  1. .node 文件路径
  2. 动态库分发
  3. asar
  4. 目标机依赖环境

14. 一页速记总结

14.1 实战模板主线

  1. Addon 负责原生封装
  2. Native Service 负责 JS 层整理
  3. Main IPC 负责权限与调度
  4. Preload 负责白名单暴露
  5. Renderer 只负责业务调用

14.2 记忆口诀

原生层做薄,服务层收口,Main 做调度,Preload 做白名单,Renderer 不碰底层。