跳到主要内容

原生代码与 Electron

Electron 最强大的特性之一是能够将 Web 技术与原生代码结合起来 - 既可用于计算密集型逻辑,也可用于偶尔需要原生用户界面的场景。

Electron 通过构建在“原生 Node.js 插件”之上来实现这一点。 您可能已经遇到过其中的一些 - 比如著名的 sqlite 等包使用原生代码来结合 JavaScript 和原生技术。 您可以使用此功能扩展您的 Electron 应用程序,实现完全原生应用程序可以做的任何事情

  • 访问 JavaScript 中不可用的原生平台 API。 任何 macOS、Windows 或 Linux 操作系统的 API 都可供您使用。
  • 创建与原生桌面框架交互的 UI 组件。
  • 集成现有的原生库。
  • 实现比 JavaScript 运行更快的性能关键代码。

原生 Node.js 插件是动态链接的共享对象 (在类 Unix 系统上) 或 DLL 文件 (在 Windows 上),可以使用 require()import 函数加载到 Node.js 或 Electron 中。 它们的功能与常规 JavaScript 模块类似,但提供了与用 C++、Rust 或其他可编译为原生代码的语言编写的代码交互的接口。

教程:为 Electron 创建原生 Node.js 插件

本教程将引导您构建一个可以在 Electron 应用程序中使用的基本 Node.js 原生插件。 我们将重点关注所有平台通用的概念,并使用 C++ 作为实现语言。 完成本教程(适用于所有原生 Node.js 插件)后,您可以继续学习我们的平台特定教程之一。

要求

本教程假设您已安装 Node.js 和 npm,以及在您的平台上编译代码所需的基本工具 (例如 Windows 上的 Visual Studio、macOS 上的 Xcode 或 Linux 上的 GCC/Clang)。 您可以在 node-gyp 的 readme 文件中找到详细说明。

要求:macOS

要在 macOS 上构建原生 Node.js 插件,您需要 Xcode Command Line Tools。 这些工具提供了必要的编译器和构建工具 (即 clangclang++make)。 如果尚未安装,以下命令将提示您安装 Command Line Tools。

xcode-select --install

要求:Windows

Node.js 官方安装程序提供了可选安装“原生模块工具”的功能,该功能会安装 C++ 模块基本编译所需的一切 - 具体来说是 Python 3 和“使用 C++ 进行桌面开发”工作负载。 或者,您可以使用 chocolateywinget 或 Windows 应用商店。

要求:Linux

1) 创建包

首先,创建一个新的 Node.js 包来包含您的原生插件

mkdir my-native-addon
cd my-native-addon
npm init -y

这将创建一个基本的 package.json 文件。 接下来,我们将安装必要的依赖项

npm install node-addon-api bindings
  • node-addon-api:这是一个用于低级 Node.js API 的 C++ 封装,使构建插件更容易。 它提供了一个 C++ 面向对象的 API,比原始的 C 风格 API 更方便、更安全。
  • bindings:一个辅助模块,简化了加载已编译原生插件的过程。 它会自动处理查找已编译的 .node 文件。

现在,让我们更新 package.json 文件以包含适当的构建脚本。 我们将在下方进一步解释这些脚本的具体作用。

package.json
{
"name": "my-native-addon",
"version": "1.0.0",
"description": "A native addon for Electron",
"main": "js/index.js",
"scripts": {
"clean": "node -e \"require('fs').rmSync('build', { recursive: true, force: true })\"",
"build": "node-gyp configure && node-gyp build"
},
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^8.3.0"
},
"devDependencies": {
"node-gyp": "^11.1.0"
}
}

这些脚本将

  • clean:移除构建目录,以便进行全新构建
  • build:运行标准的 node-gyp 构建过程来编译您的插件

2) 设置构建系统

Node.js 插件使用一个名为 node-gyp 的构建系统,这是一个用 Node.js 编写的跨平台命令行工具。 它在后台使用平台特定的构建工具为 Node.js 编译原生插件模块

  • 在 Windows 上:Visual Studio
  • 在 macOS 上:Xcode 或命令行工具
  • 在 Linux 上:GCC 或类似的编译器

配置 node-gyp

binding.gyp 文件是一个类似 JSON 的配置文件,它告诉 node-gyp 如何构建您的原生插件。 它类似于 makefile 或项目文件,但采用平台无关的格式。 让我们创建一个基本的 binding.gyp 文件

binding.gyp
{
"targets": [
{
"target_name": "my_addon",
"sources": [
"src/my_addon.cc",
"src/cpp_code.cc"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"include"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"defines": [
"NODE_ADDON_API_CPP_EXCEPTIONS"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"xcode_settings": {
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
"CLANG_CXX_LIBRARY": "libc++",
"MACOSX_DEPLOYMENT_TARGET": "10.14"
},
"msvs_settings": {
"VCCLCompilerTool": {
"ExceptionHandling": 1
}
}
}
]
}

让我们分解一下这个配置

  • target_name:您的插件名称。 这决定了编译模块的文件名 (my_addon.node)。
  • sources:要编译的源文件列表。 我们将有两个文件:主插件文件和我们实际的 C++ 实现。
  • include_dirs:搜索头文件的目录。 看起来很神秘的行 <!@(node -p \"require('node-addon-api').include\") 运行 Node.js 命令以获取 node-addon-api 的 include 目录路径。
  • dependenciesnode-addon-api 依赖项。 与 include 目录类似,这会执行一个 Node.js 命令以获取正确的配置。
  • defines:预处理器定义。 这里我们为 node-addon-api 启用 C++ 异常。 平台特定设置
  • cflags! 和 cflags_cc!:类 Unix 系统的编译器标志
  • xcode_settings:特定于 macOS/Xcode 编译器的设置
  • msvs_settings:特定于 Windows 上的 Microsoft Visual Studio 的设置

现在,为我们的项目创建目录结构

mkdir src
mkdir include
mkdir js

这将创建

  • src/:存放我们的源文件
  • include/:存放头文件
  • js/:存放我们的 JavaScript 封装器

3) C++ 版“Hello World”

让我们首先在头文件中定义我们的 C++ 接口。 创建 include/cpp_code.h

#pragma once
#include <string>

namespace cpp_code {
// A simple function that takes a string input and returns a string
std::string hello_world(const std::string& input);
} // namespace cpp_code

#pragma once 指令是一个头文件卫士,防止同一编译单元多次包含该文件。 实际的函数声明位于命名空间内,以避免潜在的名称冲突。

接下来,让我们在 src/cpp_code.cc 中实现该函数

src/cpp_code.cc
#include <string>
#include "../include/cpp_code.h"

namespace cpp_code {
std::string hello_world(const std::string& input) {
// Simply concatenate strings and return
return "Hello from C++! You said: " + input;
}
} // namespace cpp_code

这是一个简单的实现,它只是向输入字符串添加一些文本并返回。

现在,让我们创建连接 C++ 代码与 Node.js/JavaScript 世界的插件代码。 创建 src/my_addon.cc

src/my_addon.cc
#include <napi.h>
#include <string>
#include "../include/cpp_code.h"

// Create a class that will be exposed to JavaScript
class MyAddon : public Napi::ObjectWrap<MyAddon> {
public:
// This static method defines the class for JavaScript
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
// Define the JavaScript class with method(s)
Napi::Function func = DefineClass(env, "MyAddon", {
InstanceMethod("helloWorld", &MyAddon::HelloWorld)
});

// Create a persistent reference to the constructor
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);

// Set the constructor on the exports object
exports.Set("MyAddon", func);
return exports;
}

// Constructor
MyAddon(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<MyAddon>(info) {}

private:
// Method that will be exposed to JavaScript
Napi::Value HelloWorld(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

// Validate arguments (expecting one string)
if (info.Length() < 1 || !info[0].IsString()) {
Napi::TypeError::New(env, "Expected string argument").ThrowAsJavaScriptException();
return env.Null();
}

// Convert JavaScript string to C++ string
std::string input = info[0].As<Napi::String>();

// Call our C++ function
std::string result = cpp_code::hello_world(input);

// Convert C++ string back to JavaScript string and return
return Napi::String::New(env, result);
}
};

// Initialize the addon
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return MyAddon::Init(env, exports);
}

// Register the initialization function
NODE_API_MODULE(my_addon, Init)

让我们分解一下这段代码

  1. 我们定义了一个继承自 Napi::ObjectWrap<MyAddon>MyAddon 类,它负责为 JavaScript 封装我们的 C++ 类。
  2. Init 静态方法: 2.1 定义一个包含名为 helloWorld 方法的 JavaScript 类 2.2 创建构造函数的持久引用 (防止垃圾回收) 2.3 导出类构造函数
  3. 构造函数只是将其参数传递给父类。
  4. HelloWorld 方法: 4.1 获取 Napi 环境 4.2 验证输入参数 (期望一个字符串) 4.3 将 JavaScript 字符串转换为 C++ 字符串 4.4 调用我们的 C++ 函数 4.5 将结果转换回 JavaScript 字符串并返回
  5. 我们定义了一个初始化函数,并使用 NODE_API_MODULE 宏注册它,这使得我们的模块可以被 Node.js 加载。

现在,让我们创建一个 JavaScript 封装器,使插件更易于使用。 创建 js/index.js

js/index.js
const EventEmitter = require('events')

// Load the native addon using the 'bindings' module
// This will look for the compiled .node file in various places
const bindings = require('bindings')
const native = bindings('my_addon')

// Create a nice JavaScript wrapper
class MyNativeAddon extends EventEmitter {
constructor () {
super()

// Create an instance of our C++ class
this.addon = new native.MyAddon()
}

// Wrap the C++ method with a nicer JavaScript API
helloWorld (input = '') {
if (typeof input !== 'string') {
throw new TypeError('Input must be a string')
}
return this.addon.helloWorld(input)
}
}

// Export a singleton instance
if (process.platform === 'win32' || process.platform === 'darwin' || process.platform === 'linux') {
module.exports = new MyNativeAddon()
} else {
// Provide a fallback for unsupported platforms
console.warn('Native addon not supported on this platform')

module.exports = {
helloWorld: (input) => `Hello from JS! You said: ${input}`
}
}

这个 JavaScript 封装器

  1. 使用 bindings 加载我们编译好的原生插件
  2. 创建一个继承自 EventEmitter 的类 (对于未来可能发出事件的扩展很有用)
  3. 实例化我们的 C++ 类并提供更简单的 API
  4. 在 JavaScript 端添加一些输入验证
  5. 导出一个我们的封装器的单例实例
  6. 优雅地处理不支持的平台

构建和测试插件

现在我们可以构建我们的原生插件了

npm run build

这将运行 node-gyp configurenode-gyp build 将我们的 C++ 代码编译成一个 .node 文件。 让我们创建一个简单的测试脚本来验证一切正常。 在项目根目录创建 test.js

test.js
// Load our addon
const myAddon = require('./js')

// Try the helloWorld function
const result = myAddon.helloWorld('This is a test')

// Should print: "Hello from C++! You said: This is a test"
console.log(result)

运行测试

node test.js

如果一切正常,您应该会看到

Hello from C++! You said: This is a test

在 Electron 中使用插件

要在 Electron 应用程序中使用此插件,您需要

  1. 将其作为依赖项包含在您的 Electron 项目中
  2. 构建它以针对您的特定 Electron 版本。 electron-forge 会自动为您处理此步骤 - 更多详细信息,请参阅 原生 Node 模块
  3. 在启用了 Node.js 的进程中像使用任何其他模块一样导入和使用它。
// In your main process
const myAddon = require('my-native-addon')
console.log(myAddon.helloWorld('Electron'))

参考资料和进一步学习

原生插件开发可以使用除 C++ 之外的多种语言编写。 Rust 可以与 napi-rsneonnode-bindgen 等 crate 一起使用。 在 macOS 上,Objective-C/Swift 可以通过 Objective-C++ 使用。

具体实现细节因平台而异,尤其是在访问平台特定 API 或 UI 框架时,例如 Windows 的 Win32 API、COM 组件、UWP/WinRT - 或 macOS 的 Cocoa、AppKit 或 ObjectiveC 运行时。

这意味着您的原生代码可能需要参考两组资料:首先,在 Node.js 侧,使用 N-API 文档学习如何创建复杂结构并将其暴露给 JavaScript - 例如异步线程安全函数调用或创建 JavaScript 原生对象 (error, promise 等)。 其次,在使用您正在使用的技术时,您可能会查看它们的底层文档