在使用Electron开发客户端时,如果现有Node模块所提供的功能无法满足需求,我们可以使用C++开发自定义的Node模块,也称插件(addon)。
Node.js插件的扩展名为.node
,是二进制文件,其本质上是动态链接库重命名而来,在Windows平台是.dll文件,Linux/Unix平台是.so文件。
1. 选择Node-API
开发Node.js扩展的方式有三种:
- Node-API(以前叫N-API)
- nan
- 直接使用v8、libuv等库进行开发
除非是为了使用 Node-API 未公开的接口,否则建议使用 Node-API 进行开发。
因为Node-API是二进制(ABI)兼容的,它将底层JavaScript引擎与上层插件隔离开了,JavaScripty引擎的修改不会影响我们开发的上层插件,我们基于某个版本编译的插件在不需要重新编译的情况下,就可以运行在其他版本的Node.js中。
2. 安装编译环境
Node插件使用C++开发,因此在不同的系统上采用不同的编译环境。
在Linux环境通常使用GCC和LLVM;
Mac环境通常使用Xcode;
Windows环境通常使用Visual Studio,如果不想安装完整的Visual Studio,可以使用如下命令仅安装必要的工具链:
1 | npm install --global windows-build-tools |
Node插件通常使用node-gyp进行编译,node-gyp基于Google的gyp-next构建系统,node-gyp已经与npm捆绑在一起,但我们在使用node-gyp之前还需要先安装Python。
至此Node插件开发的环境已经搭建完成。
3.搭建工程
本文以在Windows下开发Node插件为例,其他系统环境在编译选项方面略有不同
3.1 package.json
1 | { |
使用
npm install
安装依赖项。
各个依赖项的作用如下:
node-addon-api
用于提供了Node-API相关的头文件;bindings
用于帮助插件开发者快速导入编译后的.node插件,方便调试,这个依赖是非必须;
build-debug
和build-release
脚本分别用于编译Debug和Release版本的插件;
test
脚本用于执行测试用例;
32位插件
指定arch为ia32(--arch=ia32
)就可以编译32位版本的Node插件。
需要注意:64位版本Node.js只能加载64位的Node插件,32位版本的Node.js也只能加载32位的Node插件,否则会报错:
1 | Error: \\?\D:\node-addon-sample\build\Debug\node-addon-sample.node is not a valid Win32 application. |
3.2 编译脚本
Node-API支持GYP和CMake.js两种编译方式,这里选择使用GYP方式。
新建binding.gyp
文件,内容如下:
1 | { |
binding.gyp中的编译选项大多与特定平台的编译器有关,具体可以查阅相关编译器文档,如Windows平台可以查询MSVC文档。
可以使用如下命令指定需要使用的 Visual Stuido 版本:
1 | npm config set msvs_version 20xx |
node-gyp官方提供了一些示例,我们可以从这些示例中获取不少灵感:
GYP官方文档:
https://gyp.gsrc.io/docs/UserDocumentation.md
3.3 第一个API
现在新建main.cpp
,在该文件中定义我们的第一个API,API名为Add
,支持传入2个整数参数,返回整数相加的和。
1 |
|
在定义完API之后,还需要将API导出,在文件末尾添加如下代码:
1 | // 导出函数 |
如果忘记导出API,加载Node插件时会报错:
1 | Error: Module did not self-register: '\\?\D:\node-addson-sample\build\Debug\node-addson-sample.node'. |
现在执行npm run build-debug
编译Debug版本插件,编译生成的node插件路径为build\Debug\node-addson-sample.node
。
3.4 测试用例
新建test.js
,测试代码如下:
1 | const sample = require("bindings")("node-addson-sample.node"); |
使用bindings
模块可以不用考虑插件的具体位置,该模块会自动帮我们在项目目录下遍历查找。
4. 数据类型
在napi.h
头文件中有很多继承自Napi::Value
的子类,这些类分别对应JavaScript中的数据类型,如:
- Napi::Boolean -> Boolean
- Napi::Number -> Number
- Napi::String -> String
- Napi::Function -> Function
- Napi::Symbol -> Symbol
- Napi::Array -> Array
- Napi::Object -> Object
Node-Api还定义Promise、Date、Buffer等数据类型。
4.1 Null和Undefined
Null和Undefined比较特殊,没有定义专门的类,由Env
类的成员函数返回。
1 | env.Null() |
4.2 创建对象
有两种方式可以用来创建指定类型的对象,以创建Boolean类型为例:
1 | Napi::Boolean::New(env, true) |
以创建一个对象数组为例介绍对象和数组的使用方法:
1 | Napi::Array result = Napi::Array::New(env); |
4.3 类型校验
Napi::Value
提供了若干方法用于判断当前对象是否为指定类型,如:
- IsUndefined
- IsNull
- IsBoolean
- IsNumber
- IsString
- IsSymbol
- IsArray
- IsObject
- IsFunction
- IsPromise
- IsBuffer
5. 异常
可以在编译脚本binding.gyp
中通过预编译宏指定是否启用C++异常:
1 | NAPI_CPP_EXCEPTIONS |
如果启用C++异常,则Napi::Error
会继承自std::exception。
1 | class Error : public ObjectReference |
在启动C++异常的情况下,从Node插件抛出异常的方式如下:
1 | throw Napi::TypeError::New(env, "Wrong number of arguments"); |
将会中断当前函数throw后面代码的执行。
TypeError继承自Error,通常用于表示与类型错误相关的异常。类似的错误类型还有RangeError
等,也可以直接抛出Error
类型的错误:
1 | throw Napi::Error::New(env, "Wrong number of arguments"); |
在没有启动C++异常的情况下,从Node插件抛出异常的方式如下,抛出异常后需要使用return语句终止下面流程的执行:
1 | Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); |
Node-API官方文档:node-addon-api doc
Node.js官方addon示例:node-addon-examples