在使用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,可以使用如下命令僅安裝必要的工具鏈:
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
{"name": "node-addson-sample","version": "1.0.0","private": true,"description": "A sample node addson sample","dependencies": {"bindings": "^1.5.0","node-addon-api": "^7.1.0"},"scripts": {"build-debug": "node-gyp --debug --arch=x64 configure rebuild","build-release": "node-gyp --release --arch=x64 configure rebuild","test": "node test.js"}
}
使用
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插件,否則會報錯:
Error: \\?\D:\node-addon-sample\build\Debug\node-addon-sample.node is not a valid Win32 application.
3.2 編譯腳本
新建binding.gyp
文件,內容如下:
{"targets": [{"target_name": "node-addson-sample", # ***.node"cflags!": [ "-fno-exceptions" ],"cflags_cc!": [ "-fno-exceptions" ],# 指定需要編譯的源文件"sources": [ "main.cpp" ],"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],# 預編譯宏"defines": [ "NAPI_CPP_EXCEPTIONS", # 在Node-API中啟用C++異常],"conditions": [[# Windows平臺編譯選項"OS == 'win'", {"configurations": {# Debug編譯選項"Debug": {# 預編譯宏"defines": [ "DEBUG", "_DEBUG" ],"cflags": [ "-g", "-O0" ],"conditions": [["target_arch=='x64'", {"msvs_configuration_platform": "x64",}],],"msvs_settings": {"VCCLCompilerTool": {"RuntimeLibrary": 1, # /MTd"Optimization": 0, # /Od, no optimization"MinimalRebuild": "false","OmitFramePointers": "false","BasicRuntimeChecks": 3, # /RTC1"AdditionalOptions": ["/EHsc"],},"VCLinkerTool": {"LinkIncremental": 2, # Enable incremental linking# 附加依賴庫"AdditionalDependencies": [],},},# 附加包含目錄"include_dirs": [],},# Debug編譯選項"Release": {# 預編譯宏"defines": [ "NDEBUG" ],"msvs_settings": {"VCCLCompilerTool": {"RuntimeLibrary": 0, # /MT"Optimization": 3, # /Ox, full optimization"FavorSizeOrSpeed": 1, # /Ot, favour speed over size"InlineFunctionExpansion": 2, # /Ob2, inline anything eligible"WholeProgramOptimization": "false", # Dsiable /GL, whole program optimization, needed for LTCG"OmitFramePointers": "true","EnableFunctionLevelLinking": "true","EnableIntrinsicFunctions": "true","RuntimeTypeInfo": "false","ExceptionHandling": "2", # /EHsc"AdditionalOptions": ["/MP", # compile across multiple CPUs],"DebugInformationFormat": 3,"AdditionalOptions": [],},"VCLibrarianTool": {"AdditionalOptions": ["/LTCG", # link time code generation],},"VCLinkerTool": {"LinkTimeCodeGeneration": 1, # link-time code generation"OptimizeReferences": 2, # /OPT:REF"EnableCOMDATFolding": 2, # /OPT:ICF"LinkIncremental": 1, # disable incremental linking# 附加依賴庫"AdditionalDependencies": [],},},# 附加包含目錄"include_dirs": [],}}},]]}]
}
binding.gyp中的編譯選項大多與特定平臺的編譯器有關,具體可以查閱相關編譯器文檔,如Windows平臺可以查詢MSVC文檔。
node-gyp官方提供了一些示例,我們可以從這些示例中獲取不少靈感:
binding.gyp-files-in-the-wild
GYP官方文檔:
https://gyp.gsrc.io/docs/UserDocumentation.md
3.3 第一個API
現在新建main.cpp
,在該文件中定義我們的第一個API,API名為Add
,支持傳入2個整數參數,返回整數相加的和。
#include <napi.h>// 同步調用
// 計算兩個整數相加結果并返回
Napi::Number Add(const Napi::CallbackInfo& info) {Napi::Env env = info.Env();if (info.Length() != 2)throw Napi::TypeError::New(env, "Wrong number of arguments");if (!info[0].IsNumber() || !info[1].IsNumber())throw Napi::TypeError::New(env, "Wrong arguments");const int ret = info[0].ToNumber().Int32Value() + info[1].ToNumber().Int32Value();return Napi::Number::New(env, ret);
}
在定義完API之后,還需要將API導出,在文件末尾添加如下代碼:
// 導出函數
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(addon, Init)
如果忘記導出API,加載Node插件時會報錯:
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
,測試代碼如下:
const sample = require("bindings")("node-addson-sample.node");console.log(sample.Add(100, 200)); // 輸出300
使用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
類的成員函數返回。
env.Null()env.Undefined()
4.2 創建對象
可以通過下面兩種方式創建指定類型的對象,以創建Boolean類型為例:
Napi::Boolean::New(env, true)
Napi::Value::From(env, false)
以創建一個對象數組為例介紹對象和數組的使用方法:
Napi::Array result = Napi::Array::New(env);
for (size_t i = 0; i < 3; i++) {Napi::Object obj = Napi::Object::New(env);obj.Set(Napi::String::New(env, "filePath"), Napi::String::New(env, "/root/" + std::to_string(i) + ".txt"));obj.Set(Napi::String::New(env, "fileSize"), Napi::Number::New(env, i * 100));result.Set(Napi::Number::New(env, i), obj);
}
4.3 類型校驗
Napi::Value
提供了若干方法用于判斷當前對象是否為指定類型,如:
- IsUndefined
- IsNull
- IsBoolean
- IsNumber
- IsString
- IsSymbol
- IsArray
- IsObject
- IsFunction
- IsPromise
- IsBuffer
5. 異常
可以在編譯腳本binding.gyp
中通過預編譯宏指定是否啟用C++異常:
NAPI_CPP_EXCEPTIONSNAPI_DISABLE_CPP_EXCEPTIONS
如果啟用C++異常,則Napi::Error
會繼承自std::exception。
class Error : public ObjectReference
#ifdef NAPI_CPP_EXCEPTIONS,public std::exception
#endif // NAPI_CPP_EXCEPTIONS
...
在啟動C++異常的情況下,從Node插件拋出異常的方式如下:
throw Napi::TypeError::New(env, "Wrong number of arguments");
當前函數throw語句后面的流程將會中斷執行。
TypeError繼承自Error,通常用于表示與類型錯誤相關的異常。類似的錯誤類型還有RangeError
等,也可以直接拋出Error
類型的錯誤:
throw Napi::Error::New(env, "Wrong number of arguments");
在沒有啟動C++異常的情況下,從Node插件拋出異常的方式如下,拋出異常后需要使用return語句終止下面流程的執行:
Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
return;
更多優質內容歡迎訪問我的個人站點:https://jiangxueqiao.com
Node-API官方文檔:node-addon-api doc