Skip to main content

原生代码和 Electron

¥Native Code and Electron

Electron 最强大的功能之一是能够将 Web 技术与原生代码相结合 - 既可用于计算密集型逻辑,也可用于偶尔的原生用户界面(如果需要)。

¥One of Electron's most powerful features is the ability to combine web technologies with native code - both for compute-intensive logic as well as for the occasional native user interface, where desired.

Electron 通过在 "原生 Node.js 插件" 之上构建来实现这一点。你可能已经遇到过其中的一些 - 像著名的 sqlite 这样的软件包使用原生代码来结合 JavaScript 和原生技术。你可以使用此功能扩展你的 Electron 应用,使之具有完全原生应用可以执行的任何功能:

¥Electron does so by building on top of "Native Node.js Addons". You've probably already come across a few of them - packages like the famous sqlite use native code to combine JavaScript and native technologies. You can use this feature to extend your Electron application with anything a fully native application can do:

  • 访问 JavaScript 中不可用的原生平台 API。任何 macOS、Windows 或 Linux 操作系统 API 都可供你使用。

    ¥Access native platform APIs not available in JavaScript. Any macOS, Windows, or Linux operating system API is available to you.

  • 创建与原生桌面框架交互的 UI 组件。

    ¥Create UI components that interact with native desktop frameworks.

  • 与现有的原生库集成。

    ¥Integrate with existing native libraries.

  • 实现比 JavaScript 运行速度更快的性能关键代码。

    ¥Implement performance-critical code that runs faster than JavaScript.

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

¥Native Node.js addons are dynamically-linked shared objects (on Unix-like systems) or DLL files (on Windows) that can be loaded into Node.js or Electron using the require() or import functions. They behave just like regular JavaScript modules but provide an interface to code written in C++, Rust, or other languages that can compile to native code.

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

¥Tutorial: Creating a Native Node.js Addon for Electron

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

¥This tutorial will walk you through building a basic Node.js native addon that can be used in Electron applications. We'll focus on concepts common to all platforms, using C++ as the implementation language. Once you complete this tutorial common to all native Node.js addons, you can move on to one of our platform-specific tutorials.

要求

¥Requirements

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

¥This tutorial assumes you have Node.js and npm installed, as well as the basic tools necessary for compiling code on your platform (like Visual Studio on Windows, Xcode on macOS, or GCC/Clang on Linux). You can find detailed instructions in the node-gyp readme.

要求:苹果系统

¥Requirements: macOS

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

¥To build native Node.js addons on macOS, you'll need the Xcode Command Line Tools. These provide the necessary compilers and build tools (namely, clang, clang++, and make). The following command will prompt you to install the Command Line Tools if they aren't already installed.

xcode-select --install

要求:Windows

¥Requirements: Windows

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

¥The official Node.js installer offers the optional installation of "Tools for Native Modules", which installs everything required for the basic compilation of C++ modules - specifically, Python 3 and the "Visual Studio Desktop development with C++" workload. Alternatively, you can use chocolatey, winget, or the Windows Store.

要求:Linux

¥Requirements: Linux

*

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

¥First, create a new Node.js package that will contain your native addon:

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

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

¥This creates a basic package.json file. Next, we'll install the necessary dependencies:

npm install node-addon-api bindings
  • node-addon-api:这是一个用于底层 Node.js API 的 C++ 封装器,可以更轻松地构建插件。它提供了一个 C++ 面向对象的 API,比原始的 C 风格 API 更方便、更安全。

    ¥node-addon-api: This is a C++ wrapper for the low-level Node.js API that makes it easier to build addons. It provides a C++ object-oriented API that's more convenient and safer to use than the raw C-style API.

  • bindings:一个辅助模块,可简化加载已编译的原生插件的过程。它会自动查找已编译的 .node 文件。

    ¥bindings: A helper module that simplifies the process of loading your compiled native addon. It handles finding your compiled .node file automatically.

现在,让我们更新 package.json 以包含适当的构建脚本。我们将在下面进一步解释这些具体做什么。

¥Now, let's update our package.json to include the appropriate build scripts. We will explain what these specifically do further below.

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"
}
}

这些脚本将:

¥These scripts will:

  • clean:删除构建目录,允许全新构建

    ¥clean: Remove the build directory, allowing for a fresh build

  • build:运行标准 node-gyp 构建过程以编译你的插件

    ¥build: Run the standard node-gyp build process to compile your addon

*

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

¥Node.js addons use a build system called node-gyp, which is a cross-platform command-line tool written in Node.js. It compiles native addon modules for Node.js using platform-specific build tools behind the scenes:

  • 在 Windows 上:Visual Studio

    ¥On Windows: Visual Studio

  • 在 macOS 上:Xcode 或命令行工具

    ¥On macOS: Xcode or command-line tools

  • 在 Linux 上:GCC 或类似的编译器

    ¥On Linux: GCC or similar compilers

配置 node-gyp

¥Configuring node-gyp

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

¥The binding.gyp file is a JSON-like configuration file that tells node-gyp how to build your native addon. It's similar to a make file or a project file but in a platform-independent format. Let's create a basic binding.gyp file:

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
}
}
}
]
}

让我们分解此配置:

¥Let's break down this configuration:

  • target_name:你的插件的名称。这决定了编译模块的文件名(my_addon.node)。

    ¥target_name: The name of your addon. This determines the filename of the compiled module (my_addon.node).

  • sources:要编译的源文件列表。我们将有两个文件:主插件文件和我们实际的 C++ 实现。

    ¥sources: List of source files to compile. We'll have two files: the main addon file and our actual C++ implementation.

  • include_dirs:搜索头文件的目录。看似神秘的 <!@(node -p \"require('node-addon-api').include\") 行运行 Node.js 命令来获取 node-addon-api 包含目录的路径。

    ¥include_dirs: Directories to search for header files. The cryptic-looking line <!@(node -p \"require('node-addon-api').include\") runs a Node.js command to get the path to the node-addon-api include directory.

  • dependenciesnode-addon-api 依赖。与包含目录类似,这会执行 Node.js 命令以获取正确的配置。

    ¥dependencies: The node-addon-api dependency. Similar to the include dirs, this executes a Node.js command to get the proper configuration.

  • defines:预处理器定义。在这里,我们为 node-addon-api 启用 C++ 异常。平台特定设置:

    ¥defines: Preprocessor definitions. Here we're enabling C++ exceptions for node-addon-api. Platform-specific settings:

  • cflags!和 cflags_cc!:类 Unix 系统的编译器标志

    ¥cflags! and cflags_cc!: Compiler flags for Unix-like systems

  • xcode_settings:特定于 macOS/Xcode 编译器的设置

    ¥xcode_settings: Settings specific to macOS/Xcode compiler

  • msvs_settings:特定于 Windows 上的 Microsoft Visual Studio 的设置

    ¥msvs_settings: Settings specific to Microsoft Visual Studio on Windows

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

¥Now, create the directory structure for our project:

mkdir src
mkdir include
mkdir js

这将创建:

¥This creates:

  • src/:我们的源文件将放在哪里

    ¥src/: Where our source files will go

  • include/:对于头文件

    ¥include/: For header files

  • js/:对于我们的 JavaScript 封装器

    ¥js/: For our JavaScript wrapper

*

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

¥Let's start by defining our C++ interface in a header file. Create 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 指令是一个头保护,可防止文件在同一个编译单元中被多次包含。实际函数声明位于命名空间内,以避免潜在的名称冲突。

¥The #pragma once directive is a header guard that prevents the file from being included multiple times in the same compilation unit. The actual function declaration is inside a namespace to avoid potential name conflicts.

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

¥Next, let's implement the function in 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

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

¥This is a simple implementation that just adds some text to the input string and returns it.

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

¥Now, let's create the addon code that bridges our C++ code with the Node.js/JavaScript world. Create 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)

让我们分解此代码:

¥Let's break down this code:

  1. 我们定义一个从 Napi::ObjectWrap<MyAddon> 继承的 MyAddon 类,它负责为 JavaScript 封装我们的 C++ 类。

    ¥We define a MyAddon class that inherits from Napi::ObjectWrap<MyAddon>, which handles wrapping our C++ class for JavaScript.

  2. Init 静态方法:2.1 定义一个带有名为 helloWorld 的方法的 JavaScript 类 2.2 创建对构造函数的持久引用(以防止垃圾收集) 2.3 导出类构造函数

    ¥The Init static method: 2.1 Defines a JavaScript class with a method called helloWorld 2.2 Creates a persistent reference to the constructor (to prevent garbage collection) 2.3 Exports the class constructor

  3. 构造函数只是将其参数传递给父类。

    ¥The constructor simply passes its arguments to the parent class.

  4. HelloWorld 方法:4.1 获取 Napi 环境 4.2 验证输入参数(需要字符串) 4.3 将 JavaScript 字符串转换为 C++ 字符串 4.4 调用我们的 C++ 函数 4.5 将结果转换回 JavaScript 字符串并返回它

    ¥The HelloWorld method: 4.1 Gets the Napi environment 4.2 Validates input arguments (expecting a string) 4.3 Converts the JavaScript string to a C++ string 4.4 Calls our C++ function 4.5 Converts the result back to a JavaScript string and returns it

  5. 我们定义一个初始化函数并用 NODE_API_MODULE 宏注册它,这使得我们的模块可以被 Node.js 加载。

    ¥We define an initialization function and register it with NODE_API_MODULE macro, which makes our module loadable by Node.js.

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

¥Now, let's create a JavaScript wrapper to make the addon easier to use. Create 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 封装器:

¥This JavaScript wrapper:

  1. 使用 bindings 加载我们编译的原生插件

    ¥Uses bindings to load our compiled native addon

  2. 创建一个扩展 EventEmitter 的类(对于可能触发事件的未来扩展很有用)

    ¥Creates a class that extends EventEmitter (useful for future extensions that might emit events)

  3. 实例化我们的 C++ 类并提供更简单的 API

    ¥Instantiates our C++ class and provides a simpler API

  4. 在 JavaScript 端添加一些输入验证

    ¥Adds some input validation on the JavaScript side

  5. 导出封装器的单例实例

    ¥Exports a singleton instance of our wrapper

  6. 句柄优雅地支持不受支持的平台

    ¥Handles unsupported platforms gracefully

构建和测试插件

¥Building and testing the addon

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

¥Now we can build our native addon:

npm run build

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

¥This will run node-gyp configure and node-gyp build to compile our C++ code into a .node file. Let's create a simple test script to verify everything works. Create test.js in the project root:

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)

运行测试:

¥Run the test:

node test.js

如果一切正常,你应该看到:

¥If everything works correctly, you should see:

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

在 Electron 中使用插件

¥Using the addon in Electron

要在 Electron 应用中使用此插件,你需要:

¥To use this addon in an Electron application, you would:

  1. 将其作为依赖包含在你的 Electron 项目中

    ¥Include it as a dependency in your Electron project

  2. 针对你的特定 Electron 版本构建它。electron-forge 会自动为你处理此步骤 - 有关更多详细信息,请参阅 原生 Node 模块

    ¥Build it targeting your specific Electron version. electron-forge handles this step automatically for you - for more details, see Native Node Modules.

  3. 在启用了 Node.js 的进程中,像任何其他模块一样导入和使用它。

    ¥Import and use it just like any other module in a process that has Node.js enabled.

// In your main process
const myAddon = require('my-native-addon')
console.log(myAddon.helloWorld('Electron'))

参考和进一步学习

¥References and further learning

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

¥Native addon development can be written in several languages beyond C++. Rust can be used with crates like napi-rs, neon, or node-bindgen. Objective-C/Swift can be used through Objective-C++ on macOS.

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

¥The specific implementation details differ significantly by platform, especially when accessing platform-specific APIs or UI frameworks, like Windows' Win32 API, COM components, UWP/WinRT - or macOS's Cocoa, AppKit, or ObjectiveC runtime.

这意味着你可能会使用两组引用来引用你的原生代码:首先,在 Node.js 方面,使用 N-API 文档 了解如何创建和向 JavaScript 公开复杂结构 - 如异步线程安全函数调用或创建 JavaScript 原生对象(errorpromise 等)。其次,就你正在使用的技术而言,你可能会查看它们的更底层文档:

¥This means that you'll likely use two groups of references for your native code: First, on the Node.js side, use the N-API documentation to learn about creating and exposing complex structures to JavaScript - like asynchronous thread-safe function calls or creating JavaScript-native objects (error, promise, etc). Secondly, on the side of the technology you're working with, you'll likely be looking at their lower-level documentation: