Node.js 10中原生模塊的未來

Node.js 10即將推出,並且有很多改進。令我們興奮的是對本地模塊庫n-api的更新。它在即將發佈的版本中脫離實驗狀態。

Node.js 10中原生模塊的未來

與其他語言相比,JavaScript總是擁有最低限度的標準庫。一開始,我們只在瀏覽器中使用JavaScript。隨著瀏覽器逐漸發展併成熟為應用程序虛擬機,需要通過瀏覽器庫添加更多功能。這帶來了新的應用程序,如Web藍牙,Web USB等; 不斷擴展我們可以使用JavaScript的東西。

一點歷史

有一次,我們意識到,由於其事件驅動性,JavaScript將成為編寫高度可擴展的服務器應用程序的絕佳語言。Node.js誕生了。一個新的最小標準庫,由用於編寫非瀏覽器應用程序(如文件系統綁定,TCP堆棧,模塊加載程序等)所需的一些基本功能組成。很難準確估計未來的用例,所以為了使平臺更加靈活,增加了用C / C ++編寫模塊的能力。這使得開發人員可以充分利用其平臺上提供的任何API,但仍然將其公開為JavaScript API供用戶使用。

許多偉大的模塊是這樣寫的。 LevelDB是一個嵌入式的快速數據庫,它被編寫為一個本地模塊,它將LevelDB C ++代碼與易於使用的JavaScript API相結合。LevelDB引發了一個生態系統,在其中開發了許多有趣的模塊和應用程序。很少有LevelDB的用戶知道模塊中的C ++是如何工作的,但幸運的是,我們不需要 - 本地模塊將所有這些都抽象出來。

隨著越來越多的人開始使用本地模塊,我們也學到了一些缺點。事實證明,它們很難維護,因為用於實現模塊的V8 API變化很大。對於用戶使用模塊,他們需要在他們的機器上安裝一個完整的編譯器堆棧(在Windows上,這個過程涉及用戶必須安裝Visual Studio!)。

Along Came NAN

為了解決不斷變化的V8 API的問題, NAN 誕生了。NAN代表“Node.js的Native Abstractions”,它是一系列抽象出不斷變化的V8 API之間差異的宏。實際上,NAN最初由Rod Vagg創建, 以幫助 LevelDB 開發。這意味著您可以使用最新版本的Node.js編寫本地模塊,並且可以在大多數以前的版本中工作,而且不會太複雜。這也意味著在大多數較新的Node.js版本中,您的舊模塊將繼續編譯。這是能夠維護原生模塊的巨大改進。

// silly NAN backed module that prints a string from c++

#include

#include

using namespace v8;

NAN_METHOD(Print) {

if (!info[0]->IsString()) return Nan::ThrowError("Must pass a string");

Nan::Utf8String path(info[0]);

printf("Printed from C++: %s\n", *path);

}

NAN_MODULE_INIT(InitAll) {

Nan::Set(target,

Nan::New("print").ToLocalChecked(),

Nan::GetFunction(Nan::New(Print)).ToLocalChecked()

);

}

NODE_MODULE(a_native_module, InitAll)

(請參閱完整的NAN示例回購)

預構建

為了避免必須安裝編譯工具鏈,在發佈模塊之前執行了一系列實驗。預編譯的二進制文件將在GitHub發佈的地方在線託管,並npm install 在模塊安裝時通過腳本下載 。如果沒有可用的預構建可用,那麼 npm install腳本將像往常一樣回退編譯模塊。

你可以在leveldown倉庫中看到這個例子 。

儘管如此,還是有很多問題繼續出現。偶爾,NAN將不得不做出向後不兼容的改變。這意味著舊模塊不會在較新版本的Node.js上編譯而不升級它們。引入新的Node.js兼容運行時間(如Electron)使情況更加複雜。使用Node.js編譯的模塊不會在Electron上運行,從而讓用戶留下不明顯的錯誤。

網絡代理和從第三方來源下載二進制文件的安全問題使預編譯下載變得困難。具有諷刺意味的是,prebuilds使得Electron也很難使用。下載的預製版針對的npm install 是正在運行的Node.js版本,而不是Electron 版本 。另外,對npm install 腳本的硬性要求 也可能是一個安全問題,因為用戶禁用這些腳本以避免 運行npm蠕蟲。

現在

NAN和prebuilds使事情變得更好; 但仍然不如我們想要的那麼好。本地模塊仍然被認為是“昂貴”的依賴關係,並且經常用作參數來包含Node.js核心與npm模塊中的某些內容。

幸運的是,事情正在迅速改善,我們已經處於現在的階段,我們擁有編寫和發佈本地模塊的工具,用戶可以在很少或沒有技術開銷的情況下安裝它們。

N-API

為了使本地模塊更易於編寫和維護,Node.js核心貢獻者一直在開發一種新的核心API,稱為n-api(或node-api)。

n-api背後的想法是在您編寫本機模塊的V8 API上構建一個穩定的接口。這種方法引入了一系列好處:

不需要重新編譯模塊,因為接口永遠不會中斷(將其視為系統調用,但是對於Node.js)。

允許V8以外的JavaScript引擎執行n-api。

只要實現n-api,就可以在Electron和其他運行時使用本機模塊。

N-api的性能影響很小,因為您必須通過一個纖細的抽象層而不是原始的V8代碼。但是,它有 很好的 文檔。

一段時間以來,n-api一直是一個非實驗性的API。它將在Node.js 10中被釋放,並且後端將會到達Node.js 8和6(雖然現在是6的實驗)。

這裡是我們的NAN例子,從上面移植到n-api:

// silly n-api backed module that prints a string from c

#include

#include

napi_value print (napi_env env, napi_callback_info info) {

napi_value argv[1];

size_t argc = 1;

napi_get_cb_info(env, info, &argc, argv, NULL, NULL);

if (argc < 1) {

napi_throw_error(env, "EINVAL", "Too few arguments");

return NULL;

}

char str[1024];

size_t str_len;

if (napi_get_value_string_utf8(env, argv[0], (char *) &str, 1024, &str_len) != napi_ok) {

napi_throw_error(env, "EINVAL", "Expected string");

return NULL;

}

printf("Printed from C: %s\n", str);

return NULL;

}

napi_value init_all (napi_env env, napi_value exports) {

napi_value print_fn;

napi_create_function(env, NULL, 0, print, NULL, &print_fn);

napi_set_named_property(env, exports, "print", print_fn);

return exports;

}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init_all)

(查看完整的n-api示例回購, 並 查看Node.js核心中的示例)

除了C API之外,還有更高級別的C ++包裝器,也稱為 node-addon-api。C ++包裝器是由n-API合作者維護的npm模塊。使用C ++包裝器,您可以獲得支持,直到Node.js 4為止,因為它具有舊版本的兼容性層。

通常,在封裝C接口時使用C API,而在封裝C ++ API時使用C ++ API。

捆綁預製

使用n-api支持預編譯變得更容易。由於n-api具有穩定的API,因此我們可以為Node.js 10預先生成一個模塊,並且它可以在Node.js 11,12和更新版本上運行。

為了解決在安裝時不得不下載預構建的問題,我和其他一些貢獻者最近發佈了一組模塊,稱為 prebuildify, node- gyp -build和 prebuildify-ci。

  1. prebuildify 將預先建立你的模塊

  2. node- gyp -build可以在安裝時測試預構建,並支持使用JavaScript API從磁盤加載預構建。

  3. prebuildify-ci 可幫助您在ci上設置prebuildify,以便為Linux 32/64位,MacOS和Windows 32/64位自動構建。

其他預建模塊存在於npm上。那麼他們與prebuildify有什麼不同呢?使用prebuildify而不是在安裝時為您的平臺下載預構建,我們只需在node_modules 發佈之前將./prebuilds文件夾中的所有平臺的所有預構建綁定 到npm。在安裝時,我們使用node-gyp-build來簡單測試模塊中捆綁的任何預構建組件是否可以加載到平臺上。如果不是的話,我們通常會把編譯器工具鏈叫做npm。

如果出於安全原因禁用安裝腳本,預構建仍然會在運行時加載。安裝腳本只是為了測試它是否工作。

當我們第一次嘗試這種方法時,我們prebuildify的合作者擔心在node_modules 文件夾內添加多個預構建的腳印 會由於更大的包裝大小而使安裝速度變慢。具有諷刺意味的是,它實際上使我們移植的所有模塊能夠更快地安裝。下載所需的下載特定預構建的所有依賴通常需要更長的時間,而不是簡單地將它們一起下載,並與模塊的其餘部分一起下載。

將prebuildify與n-api結合在一起非常合適。N-API意味著你需要做很少的預構建 - 每個平臺需要一個平臺來支持每個平臺和一個Node.js版本。當發佈新的Node.js版本時,您不需要發佈新模塊。

您可以在n-api示例回購中看到如何使用prebuildify與n-api和ci的完整示例 。

在您正在開發的平臺以外的平臺上構建預構建可能有點乏味。這就是為什麼我們創建了prebuildify-ci; 它會設置 travis 和 appveyor 來為新版本添加標籤時構建模塊。

1.首先設置你的模塊。運行 prebuildify-ci init 將設置一個 appveyor.yml 與 travis.yml 文件,當它被標記的是你的預構建模塊。在成功構建之後,它會暫時將預構建體上傳到GitHub發佈版,以便在發佈模塊之前從那裡下載。

prebuildify-ci init

2.git push 一個標籤發佈(並且不會在npm上發佈)。

git commit -am "new cool stuff"

npm version minor

git push && git push --tags

3.等待ci完成。

4.從GitHub下載發行版。

prebuildify-ci download

這應該下載並提取預構建 ./prebuilds。

5.只需將預編譯模塊發佈到npm即可。

npm publish

就是這麼簡單。

未來

對於本地模塊,未來是光明的。在一年之內,所有活躍的Node.js發行版都將支持n-api。

原生模塊可以非常強大,並且使我們能夠模塊化Node.js內核,因為它允許像 替代的tcp堆棧, 有效的bloomfilters和 現代的加密庫一樣 在內核之外構建,而不會強制用戶在安裝時進行編譯。這將幫助我們繼續將諸如LevelDB和其他非JavaScript項目的偉大事物帶入Node.js生態系統。


分享到:


相關文章: