跳转至

原生代码与 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 命令行工具。这些工具提供了必要的编译器和构建工具(即 clang、clang++ 和 make)。如果尚未安装命令行工具,以下命令将提示您进行安装。

xcode-select --install

环境要求:Windows

官方 Node.js 安装程序提供了“Tools for Native Modules”的可选安装,它会安装 C++ 模块基本编译所需的一切——具体来说,是 Python 3 和“Visual Studio Desktop development with C++”工作负载。或者,您也可以使用 chocolatey、winget 或 Windows Store。

环境要求: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 如何构建您的原生插件。它类似于 make 文件或项目文件,但采用与平台无关的格式。让我们创建一个基本的 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 包含目录的路径。
  • dependencies:node-addon-api 依赖项。与包含目录类似,它会执行一条 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. 我们定义一个 MyAddon 类,它继承自 Napi::ObjectWrap<MyAddon>,用于处理将我们的 C++ 类封装给 JavaScript。
  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 title='js/index.js' @ts-expect-error=[5] const EventEmitter = require('node: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` 加载我们编译好的原生插件
1. 创建一个继承自 EventEmitter 的类(对于未来可能发出事件的扩展很有用)
1. 实例化我们的 C++ 类并提供更简单的 API
1. 在 JavaScript 端添加一些输入验证
1. 导出我们封装的单例实例
1. 优雅地处理不支持的平台

### 构建和测试插件 {#building-and-testing-the-addon}

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

```sh
npm run build

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

```js title='test.js' @ts-expect-error=[2] // 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)

运行测试:

```sh
node test.js

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

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

在 Electron 中使用附加组件

要在 Electron 应用中使用此附加组件,你需要:

  1. 将其作为依赖项包含在你的 Electron 项目中
  2. 针对你的特定 Electron 版本进行构建。electron-forge 会自动为你处理这一步 - 更多详情,请参阅 原生 Node 模块。
  3. 在已启用 Node.js 的进程中,像使用任何其他模块一样导入并使用它。

```js @ts-expect-error=[2] // In your main process const myAddon = require('my-native-addon')

console.log(myAddon.helloWorld('Electron')) ```

参考资料与进一步学习

原生附加组件开发可以使用 C++ 之外的多种语言编写。Rust 可以配合诸如 napi-rs、neon 或 node-bindgen 等 crate 使用。Objective-C/Swift 可以通过 macOS 上的 Objective-C++ 使用。

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

这意味着你很可能需要为原生代码使用两组参考资料:首先,在 Node.js 一侧,使用 N-API 文档 来了解如何创建并向 JavaScript 暴露复杂结构 - 例如异步线程安全函数调用,或创建 JavaScript 原生对象(error、promise 等)。其次,在你所使用的技术一侧,你很可能会查看其更底层的文档:

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 el/electron