各位同学,今天咱们聊聊Electron的“野路子”玩法——Native Addons。简单来说,就是用C++给Electron插上翅膀,让你的应用飞起来!
开场白:为什么需要 Native Addons?
Electron本身基于JavaScript和Node.js,很多时候已经足够强大了。但有些场景,JS就显得力不从心了:
- 性能瓶颈: 大量计算密集型任务,例如图像处理、音视频编解码、加密解密等,JS的效率可能会拖后腿。
- 硬件交互: 直接操作底层硬件,例如访问USB设备、串口通信等,JS原生API可能不足。
- 复用现有代码: 已经有成熟的C/C++库,不想用JS重写一遍。
- 保密性: 一些核心算法不想暴露源码,C++编译后的二进制文件更难被逆向。
这时候,Native Addons就派上用场了。它可以让你用C++编写高性能模块,然后在Electron应用中像普通JS模块一样调用。
第一部分:Native Addons 的基本概念
Native Addons本质上是Node.js的插件,Electron应用可以像使用Node.js模块一样使用它们。其核心在于将C++代码编译成特定格式的动态链接库,并提供JS接口。
1. 关键技术:Node-API (N-API)
Node-API是Node.js官方提供的一套稳定的C API,用于构建Native Addons。使用N-API编写的Addons,在不同Node.js版本之间具有更好的兼容性,降低了升级维护成本。
2. 构建工具:node-gyp
node-gyp
是Node.js官方提供的构建工具,用于编译Native Addons。它会根据你的系统环境和Node.js版本,自动生成构建脚本,并调用相应的编译器(例如GCC、Visual Studio)来编译C++代码。
3. binding.gyp
文件
binding.gyp
是一个JSON格式的配置文件,用于告诉node-gyp
如何编译你的Native Addon。它定义了源文件、头文件、编译选项等信息。
第二部分:手把手创建一个 Native Addon
咱们通过一个简单的例子来演示如何创建一个Native Addon,实现一个加法函数。
1. 项目初始化
创建一个新的项目目录,例如 my-addon
,然后在该目录下执行以下命令:
npm init -y
npm install node-gyp --save-dev
2. 创建 binding.gyp
文件
在项目根目录下创建 binding.gyp
文件,内容如下:
{
"targets": [
{
"target_name": "my_addon",
"sources": [ "my_addon.cc" ],
"include_dirs": [
"<!@(node -p "require('node-addon-api').include")"
],
"dependencies": [
"<!(node -p "require('node-addon-api').gyp")"
],
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}
]
}
解释:
target_name
: 指定Addon的名称,生成的动态链接库会以这个名称命名。sources
: 指定C++源文件。include_dirs
: 指定头文件搜索路径,这里包含了node-addon-api
的头文件。dependencies
: 指定依赖项,这里依赖node-addon-api
。cflags!
: 禁止编译C++异常,提升效率。defines
: 定义宏,禁用C++异常。
3. 创建 C++ 源文件 my_addon.cc
在项目根目录下创建 my_addon.cc
文件,内容如下:
#include <napi.h>
Napi::Value Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
double a = info[0].As<Napi::Number>().DoubleValue();
double b = info[1].As<Napi::Number>().DoubleValue();
Napi::Number num = Napi::Number::New(env, a + b);
return num;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
解释:
#include <napi.h>
: 引入Node-API头文件。Add(const Napi::CallbackInfo& info)
: 加法函数,接收两个参数,返回它们的和。Napi::Env env
: 获取当前Node.js环境。info.Length()
: 获取传入参数的个数。info[0].IsNumber()
: 判断第一个参数是否为数字。info[0].As<Napi::Number>().DoubleValue()
: 将第一个参数转换为double
类型。Napi::Number::New(env, a + b)
: 创建一个新的Number
对象,值为a + b
。Init(Napi::Env env, Napi::Object exports)
: 模块初始化函数,将Add
函数导出为add
。NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
: 注册模块。
4. 构建 Native Addon
在项目根目录下执行以下命令:
node-gyp configure
node-gyp build
如果一切顺利,会在 build/Release
目录下生成 my_addon.node
文件,这就是编译好的Native Addon。
5. 在 Electron 应用中使用 Native Addon
创建一个简单的Electron应用,例如:
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const myAddon = require('./build/Release/my_addon.node');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.handle('add', async (event, a, b) => {
const result = myAddon.add(a, b);
return result;
});
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My Addon Example</title>
</head>
<body>
<h1>My Addon Example</h1>
<input type="number" id="num1" value="10">
+
<input type="number" id="num2" value="20">
=
<span id="result"></span>
<script>
const { ipcRenderer } = require('electron');
async function calculate() {
const num1 = document.getElementById('num1').value;
const num2 = document.getElementById('num2').value;
const result = await ipcRenderer.invoke('add', Number(num1), Number(num2));
document.getElementById('result').textContent = result;
}
document.getElementById('num1').addEventListener('change', calculate);
document.getElementById('num2').addEventListener('change', calculate);
calculate();
</script>
</body>
</html>
解释:
require('./build/Release/my_addon.node')
: 引入编译好的Native Addon。myAddon.add(a, b)
: 调用Addon中的add
函数。- 使用
ipcMain
和ipcRenderer
进行主进程和渲染进程的通信,将计算任务交给主进程的Native Addon执行。
6. 运行 Electron 应用
在项目根目录下执行以下命令:
npm start
如果一切正常,你应该能看到一个简单的加法计算器,计算结果由Native Addon提供。
第三部分:更高级的 Native Addons 用法
除了简单的函数调用,Native Addons还可以实现更复杂的功能,例如:
1. 异步操作
对于耗时操作,应该使用异步方式执行,避免阻塞主线程。Node-API提供了AsyncWorker
类,可以方便地实现异步任务。
// my_addon.cc
#include <napi.h>
#include <thread>
#include <chrono>
class MyWorker : public Napi::AsyncWorker {
public:
MyWorker(Napi::Function& callback, int delay)
: Napi::AsyncWorker(callback), delay_(delay) {}
~MyWorker() {}
// This function will be invoked in a worker thread
void Execute() {
// Simulate a long-running task
std::this_thread::sleep_for(std::chrono::milliseconds(delay_));
result_ = "Task completed after " + std::to_string(delay_) + "ms";
}
// This function will be invoked in the main thread
void OnOK() {
Napi::HandleScope scope(Env());
Callback().Call({Env().Null(), Napi::String::New(Env(), result_)});
}
private:
int delay_;
std::string result_;
};
Napi::Value DoSomethingAsync(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsFunction()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
int delay = info[0].As<Napi::Number>().Int32Value();
Napi::Function callback = info[1].As<Napi::Function>();
MyWorker* worker = new MyWorker(callback, delay);
worker->Queue();
return env.Undefined();
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "doSomethingAsync"), Napi::Function::New(env, DoSomethingAsync));
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');
const myAddon = require('./build/Release/my_addon.node');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
myAddon.doSomethingAsync(2000, (err, result) => {
if (err) {
console.error(err);
} else {
console.log(result); // Task completed after 2000ms
}
});
console.log("This will be printed before the result from the addon.");
}
app.whenReady().then(() => {
createWindow();
});
解释:
MyWorker
类继承自Napi::AsyncWorker
,用于执行异步任务。Execute()
方法在工作线程中执行耗时操作。OnOK()
方法在主线程中执行回调函数,传递结果。Queue()
方法将任务加入到Node.js的事件循环中。
2. 对象和类
Native Addons可以创建和操作C++对象,并在JS中访问它们的属性和方法。
// my_addon.cc
#include <napi.h>
class MyObject : public Napi::ObjectWrap<MyObject> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "MyObject", {
InstanceMethod("getValue", &MyObject::GetValue),
InstanceMethod("setValue", &MyObject::SetValue),
});
Napi::FunctionReference* constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("MyObject", func);
return exports;
}
MyObject(const Napi::CallbackInfo& info) : Napi::ObjectWrap<MyObject>(info) {
Napi::Env env = info.Env();
if (info.Length() > 0 && info[0].IsNumber()) {
this->value_ = info[0].As<Napi::Number>().DoubleValue();
} else {
this->value_ = 0;
}
}
private:
Napi::Value GetValue(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Number num = Napi::Number::New(env, this->value_);
return num;
}
Napi::Value SetValue(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 1 || !info[0].IsNumber()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
this->value_ = info[0].As<Napi::Number>().DoubleValue();
return env.Undefined();
}
double value_;
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return MyObject::Init(env, exports);
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');
const myAddon = require('./build/Release/my_addon.node');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
const myObject = new myAddon.MyObject(10);
console.log(myObject.getValue()); // 10
myObject.setValue(20);
console.log(myObject.getValue()); // 20
}
app.whenReady().then(() => {
createWindow();
});
解释:
MyObject
类继承自Napi::ObjectWrap<MyObject>
,用于创建C++对象。DefineClass()
方法定义类的属性和方法。InstanceMethod()
方法将C++方法暴露给JS。- 可以在JS中使用
new myAddon.MyObject()
创建对象,并调用其方法。
第四部分:常见问题和注意事项
- 版本兼容性: 使用Node-API可以提高兼容性,但仍然需要关注Node.js和Electron的版本更新,及时更新你的Addons。
- 内存管理: C++需要手动管理内存,注意避免内存泄漏。可以使用智能指针等技术来简化内存管理。
- 异常处理: 在C++代码中捕获异常,并将其转换为JS异常,方便JS代码处理。
- 调试: 可以使用GDB等工具调试C++代码,或者使用Node.js的调试器来调试JS和C++代码。
- 安全: 注意输入验证,避免安全漏洞。
表格总结:Node-API 常用 API
API | 描述 |
---|---|
Napi::Env |
表示Node.js环境。 |
Napi::Object |
表示JS对象。 |
Napi::String |
表示JS字符串。 |
Napi::Number |
表示JS数字。 |
Napi::Boolean |
表示JS布尔值。 |
Napi::Function |
表示JS函数。 |
Napi::Array |
表示JS数组。 |
Napi::Value |
表示JS值的基类。 |
Napi::CallbackInfo |
包含函数调用的信息,例如参数和上下文。 |
Napi::AsyncWorker |
用于执行异步任务。 |
结束语:Native Addons 的未来
Native Addons是Electron生态系统中一个重要的组成部分,它可以让你突破JS的限制,充分利用C++的强大能力。随着Node-API的不断完善和Electron的持续发展,Native Addons将会在更多场景下发挥重要作用。希望今天的讲座能帮助大家更好地理解和使用Native Addons,创造出更强大的Electron应用!
记住,能力越大,责任越大,用好你的C++超能力,打造出令人惊艳的应用吧!下次再见!