Node Addons是爲了能讓nodejs調用原生模塊而設計的機制,以前對一些常見原生模塊的重編譯作過梳理。但那都是Git上已經封裝好的Addons庫, 但若項目須要集成第三方提供的原生SDK,如何使用Nodejs去調用,以及如何打包到Electron項目中?html
項目中須要集成第三方投屏功能,合做廠商扔過來Mac和Windows SDK。文檔看了後,接口確實設計的比較簡單易用,但SDK是爲原生框架設計的,沒有現成的Node集成方案。node
Mac SDK是Framework形式,Mac開發基於OC,因此咱們選用 NodObjC 嘗試調用SDK。NodObjC能讓Node直接調用Framework暴露的接口。其自己也基於ffi
和ref
這兩個庫。python
Windows SDK比較複雜,SDK申明的接口全放置在一個頭文件中,在常規VC項目裏,咱們只需引入頭文件和核心.lib文件,便可實現調用。在看過接口封裝形式後,感受不適合使用ffi
這類JS庫,決定本身寫一個Addons來實現調用。linux
環境c++
下載NodObjc,npm install nodobjc
git
NodObjc依賴的ffi
和ref
兩個庫屬於Addons,須要根據運行環境去重編譯。先全局安裝好node-gyp
,而後根據當前是在node環境仍是Electron環境調試去重編譯。github
這裏遇到一個Node版本致使的問題,nodobjc在依賴庫的版本號上彷佛有些問題,致使依賴ref
沒法在node 10下編譯成功。經過n
安裝node 8,從新下載和編譯。objective-c
APIshell
引用npm
const $ = require('nodobjc');
$.import(frameworkPath);
$.framework('Foundation');
複製代碼
JS與OC的類型轉換
字符串: String -> NSString,var str = $('abc')
;
Number類型: Number -> NSNumber, var num = 123
;
Boolean: bool -> Bool,var isTrue = $.Yes;
回調函數: callback -> Block:
OC接口:
[[Test commonTest] startTest:@"123456" completeBlock:^(BOOL succeed, NSError *error) {
}];
複製代碼
經過NodObjc調用該OC接口:
const startTest = (data) => {
const param = $(data);
const completeBlock = $(function(self, success, err) {
console.log('test result', success, err);
}, ['v', ['?', 'B', '@']]);
commonTest('startTest', param, 'completeBlock', completeBlock);
}
複製代碼
NodObjc的文檔並非很詳細,須要必定的OC語法基礎。固然,花點時間看下NodObjc的源碼也能知道具體調用方式。
編譯與打包
打包有兩個注意點,一是須要經過node-gyp將ffi和ref庫重編譯生成Electron環境下可用的.node。二是適應electron-builder
的打包規則。
有問題的打包方案:
直接打包到app.asar
,打完包會發現js沒法引用Framework
經過配置electron-builder
打包規則,將Framework全部文件打包到app.asar.unpack
目錄。講道理,按以前的經驗,這樣應該就能夠了。結果直接在打包的簽名這步GG了。看上去electron-builder
沒法對二進制文件進去簽名:
***.framework, bundle format unrecognized, invalid, or unsuitable
複製代碼
解決方案
最後,選擇用extraResources
字段,將Framework從打包文件中抽出來,直接複製到Mac應用的Resources目錄下,而後在調用的js文件中,根據運行環境動態選擇調用路徑:
const path = require('path');
const isDev = process.env.NODE_ENV == 'dev';
const devPath = path.resolve(__dirname, 'Test.framework');
const prodPath = path.resolve(__dirname, 'Test.framework').replace('app.asar/src', 'src');
// const frameworkPath = path.resolve(__dirname, 'HPOfficeCastWork.framework');
const path = isDev ? devPath : prodPath;
const frameworkPath = require(path);
複製代碼
windows方面,咱們先本身按照Node Addons的開發規則來實現一個.node文件,後面直接調用.node來實現功能。
環境
首先配好windows的node-gyp編譯環境。先下載安裝python2.7和windows-build-tools。將python路徑配置到系統環境。
由於要編譯C++程序,須要保證系統已安裝好C++相關的組件。C++組件缺失會在Addons模塊編譯的時候報錯,根據具體錯誤內容,下載對應缺失組件便可。
在node環境下寫模塊demo的時候,編譯報錯:沒法解析外部符號。這個錯一開始覺得是某一塊的語法有問題,實際上是由於SDK提供的lib是32位,而咱們編譯的node環境是64位。從新安裝和配置32位的Node環境即能解決這個問題。
binding.gyp
node-gyp經過binding.gyp
文件配置模塊的編譯,所以,先了解好.gyp
的屬性頗有必要:gyp3.org
{
"targets": [
"target_name": "test",
"sources": ["test.cc"],
"include_dirs": [
"inc",
"<!(node -e \"require('nan')\")",
],
"libraries": [
"../lib/sdk.lib"
],
"conditions": [
[
"OS='win'", {
"copies": [
{
"destination": "<(PRODUCT_DIR)",
"files": [
"<(DLL_ROOT)/dnssd.dll",
"<(DLL_ROOT)/avutil.dll",
...
]
}
]
}
]
]
]
}
複製代碼
結合實際開發,介紹幾個經常使用字段:
include_dirs
: 要用到的頭文件所在目錄,<!(node -e \"require('nan')\")
用於引入nan
的頭文件,<!
是命令行擴展,gyp會將後面的字符經過shell執行。
conditions
,天然是判斷條件,一般咱們經過OS
字段來判斷當前的操做系統環境,對應的值是win
,mac
和linux
。
copies
是爲了執行文件的拷貝。起初,沒有加這段配置,編譯成功後,咱們引用生成的.node會報the specified module could not be found
錯誤。這裏能夠經過工具dependency walker
分析.node文件,查看缺失的依賴。通常這種狀況,將相關dll文件放到.node同級目錄便可。 因而,經過copies
能夠在編譯後將指定文件複製到目標目錄。<(PRODUCT_DIR)
即表示.node生成後的目錄。
Addons開發
Addons是Node提供的動態連接共享對象,具備C/C++類庫的調用能力。bingding.gyp中,咱們配置了sources
字段的值test.cc。在test.cc中,咱們經過引入v8.h
, node.h
, SDK提供的頭文件等來實現對C++接口的調用。
#include <node.h>
#include <nan.h>
#include "inc/test.h"
namespace test
{
using namespace v8;
using namespace test;
static IMirror *pobMirror = 0;
void initSdk(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
std::string appKey = *Nan::Utf8String(args[0]);
std::string pinCode = *Nan::Utf8String(args[1]);
std::string userId = *Nan::Utf8String(args[2]);
std::string serverAddr = *Nan::Utf8String(args[3]);
unsigned int serverPort = args[4]->Uint32Value();
bool isEnterprise = args[5]->BooleanValue();
Local<Function> cb = Local<Function>::Cast(args[6]);
emRtn = pobMirror->Start(appKey, pinCode, userId, serverAddr,
serverPort, isEnterprise);
const unsigned argc = 1;
Local<Value> argv[argc] = {String::NewFromUtf8(isolate, ToString(emRtn))};
cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "initSdk", InitSdk);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace test
複製代碼
js調用Addons的接口,傳遞進來的是v8數據類型,而咱們調用SDK接口,須要將其轉爲C++數據類型。這裏引用了Nan的類型轉換方法:
std::string serverAddr = *Nan::Utf8String(args[3]);
unsigned int serverPort = args[4]->Uint32Value();
bool isEnterprise = args[5]->BooleanValue();
複製代碼
對於回調函數,在轉爲Local<Function>
類型後,經過Call方法觸發回調:
Local<Function> cb = Local<Function>::Cast(args[6]);
const unsigned argc = 1;
Local<Value> argv[argc] = {String::NewFromUtf8(isolate, ToString(emRtn))};
cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
複製代碼
打包
和mac相似,若是直接將SDK和源碼一塊兒打包,electron是沒法引入SDK的。因windows暫時沒有簽名,咱們直接經過asarUnpack
將SDK文件打包到app.asar.unpack
目錄。