NodeJs Addons是為了能讓nodejs調(diào)用原生模塊而設(shè)計的機制众眨,之前對一些常見原生模塊的重編譯做過梳理慧瘤。但那都是Git上已經(jīng)封裝好的nodejs Addons庫俺泣, 但若項目需要集成第三方提供的原生SDK距潘,如何使用Nodejs去調(diào)用蜓氨,以及如何打包到Electron項目中?
背景
項目中需要集成第三方投屏功能南蓬,合作廠商扔過來Mac和Windows SDK纺非。文檔看了后哑了,接口確實設(shè)計的比較簡單易用,但SDK是為原生框架設(shè)計的烧颖,沒有現(xiàn)成的Node集成方案弱左。
Mac SDK是Framework形式,Mac開發(fā)基于OC炕淮,所以我們選用 NodObjC 嘗試調(diào)用SDK拆火。NodObjC能讓Node直接調(diào)用Framework暴露的接口。其本身也基于ffi
和ref
這兩個庫涂圆。
Windows SDK比較復(fù)雜们镜,SDK申明的接口全放置在一個頭文件中,在常規(guī)VC項目里润歉,我們只需引入頭文件和核心.lib文件模狭,即可實現(xiàn)調(diào)用。這里踩衩,我們看了接口封裝形式嚼鹉,感覺不適合使用ffi
這類JS庫,決定自己寫一個Addons來實現(xiàn)調(diào)用驱富。
Mac
環(huán)境
下載NodObjc锚赤,npm install nodobjc
NodObjc依賴的ffi
和ref
兩個庫屬于Addons,需要根據(jù)運行環(huán)境去重編譯褐鸥。需要先全局安裝好node-gyp
线脚,然后根據(jù)當(dāng)前是在node環(huán)境還是Electron環(huán)境調(diào)試去重編譯。
這里遇到一個Node版本導(dǎo)致的問題叫榕,nodobjc在依賴庫的版本號上似乎有些問題浑侥,導(dǎo)致依賴ref
無法在node 10下編譯成功。通過n
安裝node 8翠霍,重新下載和編譯锭吨。
API
-
引用
const $ = require('nodobjc'); $.import(frameworkPath); $.framework('Foundation');
-
JS與OC的類型轉(zhuǎn)換
字符串: String -> NSString,
var str = $('abc')
;Number類型: Number -> NSNumber寒匙,
var num = 123
;Boolean: bool -> Bool零如,
var isTrue = $.Yes;
回調(diào)函數(shù): callback -> Block:
OC接口:
[[Test commonTest] startTest:@"123456" completeBlock:^(BOOL succeed, NSError *error) { }];
通過NodObjc調(diào)用該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語法基礎(chǔ)锄弱。當(dāng)然考蕾,花點時間看下NodObjc的源碼也能知道具體調(diào)用方式。
-
編譯與打包
打包有兩個注意點会宪,一是需要通過node-gyp將ffi和ref庫重編譯生成Electron環(huán)境下可用的.node肖卧。二是適應(yīng)
electron-builder
的打包規(guī)則。有問題的打包方案:
直接打包到
app.asar
掸鹅,打完包會發(fā)現(xiàn)js無法引用Framework-
通過配置
electron-builder
打包規(guī)則塞帐,將Framework所有文件打包到app.asar.unpack
目錄拦赠。講道理,按之前的經(jīng)驗葵姥,這樣應(yīng)該就可以了荷鼠。結(jié)果直接在打包的簽名這步GG了±菩遥看上去electron-builder
無法對二進制文件進去簽名:***.framework, bundle format unrecognized, invalid, or unsuitable
解決方案
最后允乐,選擇用
extraResources
字段,將Framework從打包文件中抽出來削咆,直接復(fù)制到Mac應(yīng)用的Resources目錄下牍疏,然后在調(diào)用的js文件中,根據(jù)運行環(huán)境動態(tài)選擇調(diào)用路徑: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
windows方面拨齐,我們先自己按照Node Addons的開發(fā)規(guī)則來實現(xiàn)一個.node文件鳞陨,后面直接調(diào)用.node來實現(xiàn)功能。
環(huán)境
首先配好windows的node-gyp編譯環(huán)境瞻惋。先下載安裝python2.7和windows-build-tools炊邦。將python路徑配置到系統(tǒng)環(huán)境。
因為要編譯C++程序熟史,需要保證系統(tǒng)已安裝好C++相關(guān)的組件。C++組件缺失會在Addons模塊編譯的時候報錯窄俏,根據(jù)具體錯誤內(nèi)容蹂匹,下載對應(yīng)缺失組件即可。
在node環(huán)境下寫模塊demo的時候凹蜈,編譯報錯:無法解析外部符號限寞。這個錯一開始以為是某一塊的語法有問題,其實是因為SDK提供的lib是32位仰坦,而我們編譯的node環(huán)境是64位履植。重新安裝和配置32位的Node環(huán)境即能解決這個問題。
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",
...
]
}
]
}
]
]
]
}
結(jié)合實際開發(fā),介紹幾個常用字段:
include_dirs
: 要用到的頭文件所在目錄妈橄,<!(node -e \"require('nan')\")
用于引入nan
的頭文件庶近,<!
是命令行擴展,gyp會將后面的字符通過shell執(zhí)行眷蚓。
conditions
鼻种,自然是判斷條件,通常我們通過OS
字段來判斷當(dāng)前的操作系統(tǒng)環(huán)境沙热,對應(yīng)的值是win
叉钥,mac
和linux
罢缸。
copies
是為了執(zhí)行文件的拷貝。起初投队,沒有加這段配置枫疆,編譯成功后,我們引用生成的.node會報the specified module could not be found
錯誤蛾洛。這里可以通過工具dependency walker
分析.node文件养铸,查看缺失的依賴。一般這種情況轧膘,將相關(guān)dll文件放到.node同級目錄即可钞螟。 于是,通過copies
可以在編譯后將指定文件復(fù)制到目標(biāo)目錄谎碍。<(PRODUCT_DIR)
即表示.node生成后的目錄鳞滨。
Addons開發(fā)
Addonss是Node提供的動態(tài)鏈接共享對象,具有C/C++類庫的調(diào)用能力蟆淀。bingding.gyp中拯啦,我們配置了sources
字段的值test.cc。在test.cc中熔任,我們通過引入v8.h
, node.h
, SDK提供的頭文件等來實現(xiàn)對C++接口的調(diào)用褒链。
#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 hpcast
js調(diào)用Addons的接口,傳遞進來的是v8數(shù)據(jù)類型疑苔,而我們調(diào)用SDK接口甫匹,需要將其轉(zhuǎn)為C++數(shù)據(jù)類型。這里引用了Nan的類型轉(zhuǎn)換方法:
std::string serverAddr = *Nan::Utf8String(args[3]);
unsigned int serverPort = args[4]->Uint32Value();
bool isEnterprise = args[5]->BooleanValue();
對于回調(diào)函數(shù)惦费,在轉(zhuǎn)為Local<Function>
類型后兵迅,通過Call方法觸發(fā)回調(diào):
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
目錄瞧省。