精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

用 Addon 增強 Node.js 和 Electron 應(yīng)用的原生能力

開發(fā)
本文將圍繞 Node.js Addon 進行介紹,即創(chuàng)建一個 Bindings 來增強 Node.js 或 Electron 應(yīng)用的原生能力,使其可以和系統(tǒng)進行交互,或者使用一些基于 C/C++ 編寫的第三方庫。

前言

Node.js Addon 是 Node.js 中為 JavaScript 環(huán)境提供 C/C++ 交互能力的機制。其形態(tài)十分類似 Java 的 JNI,都是通過提供一套 C/C++ SDK,用于在 C/C++ 中創(chuàng)建函數(shù)方法、進行數(shù)據(jù)轉(zhuǎn)換,以便 JavaScript / Java 等語言進行調(diào)用。這樣編寫的代碼通常叫做 Bindings。

此外還有基于 C ABI Calling Convention (例如 stdcall / System-V 等標準) 直接進行跨語言調(diào)用的方案,例如 Rust FFI、Python 的 ctypes、Node.js 的 ffi 包等。這兩者的差別在于 Rust 等原生語言是直接針對平臺來將函數(shù)調(diào)用編譯為機器碼,而 ctypes 和 ffi 包則是基于 libffi 動態(tài)生成機器碼來完成函數(shù)調(diào)用的。和 Node.js Addon 的差別則在于調(diào)用和類型轉(zhuǎn)換的開銷上。

本文將圍繞 Node.js Addon 進行介紹,即創(chuàng)建一個 Bindings 來增強 Node.js 或 Electron 應(yīng)用的原生能力,使其可以和系統(tǒng)進行交互,或者使用一些基于 C/C++ 編寫的第三方庫。

Node.js 和 Electron 的關(guān)系

Electron 在主進程和渲染進程中都包含了完整的 Node.js 環(huán)境,因此本文既適用于 Node.js 程序,也適用于 Electron 程序。

Node.js Addon 的類型

在 Node.js 的 Addon,有三種類型:

圖片

本文主要介紹 Node-API 的原理,以及以 node-addon-api 作為例子。

Node-API 基本原理

圖片

Node.js 本質(zhì)上是一個動態(tài)鏈接庫(即 Windows 下的 .dll 文件、MacOS 下的 .dylib 文件、Linux 下的 .so 文件),只不過在分發(fā)時會將文件的擴展名改為 .node

加載

Node.js Addon 通常通過 CommonJS 的 require 函數(shù)進行導(dǎo)入和初始化。require 在被 .node 擴展名路徑作為參數(shù)進行調(diào)用的情況下,最終會利用 dlopen(Windows 下是 LoadLibrary)方法來動態(tài)加載這個以 .node 擴展名的動態(tài)鏈接庫:

圖片

初始化

以 https://github.com/nodejs/node-addon-examples/blob/main/1_hello_world/napi/hello.c 作為參考:

static napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

NAPI_MODULE 宏用來綁定一個 C 函數(shù)作為初始化函數(shù)。這個函數(shù)中可以用來給模塊的 exports 對象添加所需要的功能。

例如上述的代碼中,給 exports 添加了一個叫做 hello 的函數(shù)。這樣一來,我們在 Node.js 中 require 這個模塊之后,就能獲得到一個包含 hello 函數(shù)的 exports 對象:

圖片

調(diào)用

以 https://github.com/nodejs/node-addon-examples/blob/main/1_hello_world/napi/hello.c 作為參考:

static napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  assert(status == napi_ok);
  return world;
}

Method 本身是一個 C 函數(shù),接受 napi_env 作為 JavaScript 的上下文信息。napi_callback_info 作為當(dāng)前函數(shù)調(diào)用的信息,例如函數(shù)參數(shù)等。返回一個 napi_value 作為函數(shù)的返回結(jié)果。

從這個函數(shù)的例子中可以看到,在 C 中是可以獲取到函數(shù)的調(diào)用參數(shù),并且產(chǎn)生一個值作為函數(shù)的返回結(jié)果。稍后我們會以 node-addon-api 作為例子來具體介紹其編寫方式。

圖片

模塊編寫指南

本節(jié)介紹使用 C++ 配合 node-addon-api 開發(fā)模塊時常見的一些模式和樣板代碼,僅供參考。

更多用法詳見官方文檔:https://github.com/nodejs/node-addon-api/blob/main/doc/hierarchy.md

模塊初始化

使用 NODE_API_MODULE 宏綁定一個 C++ 函數(shù)進行模塊初始化:

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"),
              Napi::Function::New(env, Method));
  return exports;
}

NODE_API_MODULE(hello, Init)
  • 其中 Napi::Env 是對 napi_env 的封裝,代表一個 JavaScript 上下文,大部分和 JavaScript 交互的場景都需要這個上下文,可以保存起來以供下次使用(但是不要跨線程使用)。
  • Napi::Object exports 則是這個模塊的 exports 對象,可以把想要給 JavaScript 暴露的值和函數(shù)都設(shè)置到這個上面。

創(chuàng)建 JavaScript 函數(shù)

首先需要創(chuàng)建一個如下函數(shù)簽名的 C++ 函數(shù):

Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  double arg0 = info[0].As<Napi::Number>().DoubleValue();
  double arg1 = info[1].As<Napi::Number>().DoubleValue();
  Napi::Number num = Napi::Number::New(env, arg0 + arg1);
  return num;
}

其中函數(shù)的返回值可以是任何派生自 Napi::Value 的類型,也可以是 Napi::Value 本身。

獲取函數(shù)參數(shù)

通過 Napi::CallbackInfo& 來獲取函數(shù)參數(shù),例如 info[0] 代表第一個參數(shù)。

info[n] 會獲取一個 Napi::Value 值,我們需要調(diào)用它的 As<T> 方法來轉(zhuǎn)換為具體的值,我們才能將它繼續(xù)轉(zhuǎn)換為 C/C++ 可用的數(shù)據(jù)類型。例如,我們希望將函數(shù)的第一個參數(shù)轉(zhuǎn)換為字符串,我們需要經(jīng)過兩個步驟:

  1. 將 Napi::Value 轉(zhuǎn)換為 Napi::String:
Napi::String js_str = info[0].As<Napi::String>();
  1. 將 Napi::String 轉(zhuǎn)換為 std::string
std::string cpp_str = js_str.Utf8Value();

其他數(shù)據(jù)類型例如 Napi::NumberNapi::Buffer<T> 均有類似的方法。

返回函數(shù)結(jié)果

我們可以直接創(chuàng)建一個 JavaScript 值并在 C++ 函數(shù)中返回。具體創(chuàng)建值的方法詳見下一小節(jié)。

創(chuàng)建 JavaScript 值

我們可以利用各種實例化方法,來從 C/C++ 的數(shù)據(jù)類型中創(chuàng)建 JavaScript 的值,下面舉幾個常見的例子。

創(chuàng)建字符串

Napi::String::New(env, "字符串內(nèi)容")

創(chuàng)建數(shù)字

Napi::Number::New(env, 123)

創(chuàng)建 Buffer

創(chuàng)建 Buffer 是一個有風(fēng)險的操作。Node-API 提供了兩種創(chuàng)建方式:

  • 提供一個指針和數(shù)據(jù)長度,創(chuàng)建一個數(shù)據(jù)的拷貝
  • ? 安全,首選這種方法
  • ? v8 會負責(zé)這個 Buffer 的垃圾回收
Napi::Buffer::Copy(napi_env env, const T* data, size_t length)
  • 直接基于指針和數(shù)據(jù)長度創(chuàng)建一個 External Buffer
  • ?? 同一個指針(相同的內(nèi)存地址)只能創(chuàng)建一個 Buffer,重復(fù)創(chuàng)建會引起錯誤
  • ?? v8 / Node.js 不負責(zé)這個 Buffer 的內(nèi)存管理
Napi::Buffer::New(napi_env env, const T* data, size_t length)

異步代碼

異步函數(shù)

異步函數(shù)通常用于實現(xiàn)一些異步 IO 任務(wù)、事件,例如實現(xiàn)一個異步網(wǎng)絡(luò)請求庫的綁定。

異步函數(shù)通常有兩種實現(xiàn)方式:回調(diào) 和 Promise。

同線程回調(diào)

同線程回調(diào)的使用場景比較少:

  • 使用了 libuv 來運行了一些異步任務(wù),并且這個異步任務(wù)會在 libuv 主線程喚醒事件循環(huán)來返回結(jié)果,這時候可以比較安全地直接進行同線程回調(diào)。但是要求事先把 Napi::Env 保存在一個地方。
  • 實現(xiàn)一個函數(shù)的時候,在實現(xiàn)中直接同步調(diào)用一個 Napi::Function。
獲取函數(shù)

通常我們會從函數(shù)調(diào)用的參數(shù)中獲取到 Napi::Function,一般來說我們需要在當(dāng)次調(diào)用就把這個函數(shù)給使用掉,避免后續(xù)被 v8 GC 回收。

持久化函數(shù)

如果我們確實需要在之后的其他時機去使用函數(shù),我們需要將它通過 Napi::Persistent 持久化:

Napi::FunctionReference func_persist = Napi::Persistent(func);

使用時,可以作為一個正常的函數(shù)去使用。

調(diào)用函數(shù)

無論是 Napi::Function 還是 Napi::FunctionReference,我們都可以通過 Call 方法來調(diào)用:

Napi::Value ret_val = func_persist.Call({
  Napi::String::New(env, "Arg0")
});
跨線程回調(diào)

跨線程回調(diào)是比較常見使用場景,因為我們通常會想在另外一個線程調(diào)用 JavaScript 函數(shù)。

使用線程安全函數(shù) (ThreadSafeFunction)

為了在其他線程中調(diào)用 JavaScript 函數(shù),我們需要基于 Napi::Function 去創(chuàng)建一個 Napi::ThreadSafeFunction

Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
  env,                     // Napi::Env
  info[0].As<Function>(),  // JavaScript 函數(shù)
  "handler",               // 異步函數(shù)的名稱,用于調(diào)試的識別
  0,                       // 隊列最大大小,通常指定為 0 代表沒有限制。如果隊列已滿則可能會導(dǎo)致調(diào)用時阻塞。
  1                        // 初始線程數(shù)量,通常指定為 1。實際上是作為內(nèi)存管理使用。可參考這篇文檔。
);

接著就可以把 tsfn 保存在任何位置,并且并不需要同時保存一份 Napi::Env

調(diào)用線程安全函數(shù)

調(diào)用線程函數(shù)有兩種形式,一種是同步調(diào)用,另一種是異步調(diào)用。

同步調(diào)用

同步調(diào)用指的是如果我們限制了 ThreadSafeFunction 的隊列大小,并對其進行了多次調(diào)用,從而創(chuàng)建了許多調(diào)用任務(wù),則會導(dǎo)致隊列已滿,調(diào)用就會被阻塞,直到成功插入隊列后返回結(jié)果。

這是進行一次同步調(diào)用的例子:

const char* value = "hello world";
napi_status status = tsfn.BlockingCall(value, [](Napi::Env env, Napi::Function callback, const char* value) {
  Napi::String arg0 = Napi::String::New(env, value);
  callback.Call({ arg0 });
});

這樣一來就能順利地在任意線程去調(diào)用 JavaScript 函數(shù)。

但是我們發(fā)現(xiàn),實際上我們并不能同步地獲取函數(shù)調(diào)用的返回結(jié)果。并且 Node-API 或者 node-addon-api 都沒有提供這么一種機制。但是我們可以借助 libuv 的信號量來達到這個目的。

uv_sem_t sem;
uv_sem_init(&sem, 0);
const char* value = "hello world";
Napi::Value ret_val;
napi_status status = tsfn.BlockingCall(value, [&ret_val](Napi::Env env, Napi::Function callback, const char* value) {
  Napi::String arg0 = Napi::String::New(env, value);
  *ret_val = callback.Call({ arg0 });
  uv_sem_post(&sem);
});
uv_sem_wait(&sem);

// 直至 JavaScript 運行結(jié)束并返回結(jié)果,才會走到這里
// 這里就可以直接使用 ret_val 了

異步調(diào)用

異步調(diào)用則會在隊列已滿時直接返回錯誤狀態(tài)而不進行函數(shù)調(diào)用。除此之外的使用方法同 “同步調(diào)用” 完全一致:

const char* value = "hello world";
napi_status status = tsfn.NonBlockingCall(value, [](Napi::Env env, Napi::Function callback, const char* value) {
  Napi::String arg0 = Napi::String::New(env, value);
  callback.Call({ arg0 });
});
Promise
C++ 中創(chuàng)建 Promise 給 JavaScript 使用

我們通常會需要在 C++ 中實現(xiàn)異步函數(shù)。除了直接用上面已經(jīng)介紹的基于回調(diào)的方法之外,我們還可以直接在 C++ 中創(chuàng)建一個 Promise。

Promise 只支持同 線程 調(diào)用

由于 Promise 并未提供跨線程 Resolve 的方式,因此如果希望在其他線程對 Promise 進行 Resolve 操作,則需要結(jié)合 libuv 來實現(xiàn)。此方法比較繁瑣,建議轉(zhuǎn)而使用跨線程回調(diào)函數(shù)。如果讀者感興趣,后續(xù)本文可以補充相關(guān)內(nèi)容。

我們可以直接創(chuàng)建一個 Promise,并在函數(shù)中返回:

Napi::Value YourFunction(const Napi::CallbackInfo& info) {
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(info.Env());

  // 我們可以把 env 和 Napi::Promise::Deferred 保存在任何地方。
  // deferred_ 會在 Resolve 或者 Reject 之后釋放。
  env_ = info.Env();
  deferred_ = deferred;

  return deferred.Promise();
}

接著我們可以在其他地方調(diào)用 Napi::Promise::Deferred 來完成 Promise。注意,這里一定需要在主線程中調(diào)用:

// 返回成功結(jié)果
deferred_.Resolve(Napi::String::New(info.Env(), "OK"));
// 返回錯誤
deferred_.Reject(Napi::String::New(info.Env(), "Error"));
C++ 中使用來自 JavaScript 的 Promise

由于 Node-API 或者 node-addon-api 均沒有提供使用 Promise 的封裝,因此我們需要像在 JavaScript 中通過 .then 手動使用 Promise 的方式,在 C++ 中使用 Promise。

// 首先需要定義兩個函數(shù),用來接受 Promise 成功和失敗
Napi::Value ThenCallback(const Napi::CallbackInfo &info) { 
  Napi::Value result = info[0];
  // result 是 Promise 的返回結(jié)果
  return info.Env().Undefined();
}
Napi::Value CatchCallback(const Napi::CallbackInfo &info) { 
  Napi::Value error = info[0];
  // error 是 Promise 的錯誤信息
  return info.Env().Undefined();
}

Napi::Promise promise = async_function.Call({}).As<Napi::Promise>()
Napi::Value then_func = promise.Get("then").As<Napi::Function>();
then_func.Call(promise, { Napi::Function::New(env, ThenCallback, "then_callback") });
Napi::Value catch_func = promise.Get("catch").As<Napi::Function>();
catch_func.Call(promise, { Napi::Function::New(env, CatchCallback, "catch_callback") });

顯然這種使用方式是比較繁瑣的,我們也可以通過一些辦法使其可以將 C++ Lambda 作為回調(diào)函數(shù)來使用,但是本文暫時不涉及這部分內(nèi)容。

異步任務(wù)

異步任務(wù)通常是利用 libuv 提供的線程池來運行一些 CPU 密集型的工作。而對于一些跨線程異步回調(diào)的 Bindings 實現(xiàn)則直接使用 ThreadSafeFunction 即可。

具體使用可以參考:https://github.com/nodejs/node-addon-api/blob/main/doc/async_worker.md

Node-API 的構(gòu)建

基本構(gòu)建配置

Node.js Addon 通常使用 node-gyp 構(gòu)建,這是一個基于 Google 的 gyp 構(gòu)建系統(tǒng)實現(xiàn)的構(gòu)建工具。至于為何是 gyp,因為 Node.js 是基于 gyp 構(gòu)建的。

我們來看一個 node-addon-api 項目的構(gòu)建配置,以 bindings.gyp 命名:

{
  "targets": [
    {
      "target_name": "hello",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      "sources": [ "hello.cc" ],
      "include_dirs": [
        "<!@(node -p "require('node-addon-api').include")"
      ],
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

具體配置可以參考官方使用文檔:https://gyp.gsrc.io/docs/UserDocumentation.md

一些常識:

  • "sources" 中需要包含所有 C/C++ 代碼文件,不需要包含頭文件
  • "<!@(node -p "require('node-addon-api').include")" 在使用 Node-API 還是 node-addon-api 的情況下是不同的。
  • "target_name" 通常需要修改為你希望使用的擴展名稱,它會影響編譯產(chǎn)物的名稱。

常用構(gòu)建命令

  • node-gyp rebuild 重新構(gòu)建,會清理掉已有的構(gòu)建緩存,推薦每次都使用這個命令來構(gòu)建產(chǎn)物,避免出現(xiàn)奇怪的問題
  • 可以添加 --arch <ARCH> 參數(shù)來指定構(gòu)建的目標架構(gòu),例如希望構(gòu)建一個 32 位的產(chǎn)物,則可以使用 --arch ia32 來構(gòu)建。
  • node-gyp clean 清理構(gòu)建緩存。如果希望使用 node-gyp build 來進行構(gòu)建的話,需要善用 clean 功能。

實用構(gòu)建配置

添加頭文件目錄

'include_dirs': [
  'win32/x64/include'
]

在 Windows 下進行動態(tài)鏈接 / 靜態(tài)鏈接

'libraries': [
  'some_library.lib'
]
  • 對于動態(tài)鏈接,需要指定 .dll 對應(yīng)的 .lib 文件,并在分發(fā)的時候?qū)?.dll 放在 .node 相同的目錄下。
  • 對于靜態(tài)鏈接,則直接指定 .lib 文件即可。但是在 Node.js Addon 中進行靜態(tài)鏈接是一個比較費勁的事情,因為通常涉及到對其他靜態(tài)依賴的管理,需要謹慎選擇此方案。

在 Windows 下設(shè)置 C++ 版本

'msvs_settings': {
  'VCCLCompilerTool': {
    'AdditionalOptions': [
      '/std:c++20'
    ]
  }
}

在 Windows MSVC 下構(gòu)建支持代碼文件中的 UTF-8 字符(中文注釋等)

本質(zhì)上是給 MSVC 的編譯器添加一個 /utf-8 參數(shù)

'msvs_settings': {
  'VCCLCompilerTool': {
    "AdditionalOptions": [
      '/utf-8'
    ]
  }
}

在 MacOS 下進行動態(tài)鏈接 / 靜態(tài)鏈接

'link_settings': {
  'libraries': [
    '-L<動態(tài)庫或靜態(tài)庫所在的文件夾>',
    '-l<動態(tài)庫名稱>'
  ]
}

在 MacOS 下引入系統(tǒng) Framework 依賴

'libraries': [
  '-framework MediaPlayer',
  '-framework Cocoa',
]

在 MacOS 下設(shè)置 C++ 版本

"cflags_cc": [
  "-std=c++20"
]

在 MacOS Xcode 的 Release 構(gòu)建下生成 .dSYM 調(diào)試文件

'xcode_settings': {
  'DEBUG_INFORMATION_FORMAT': 'dwarf-with-dsym'
}

使 MacOS 下的 addon 能夠使用同目錄下的動態(tài)庫 / Framework

'link_settings': {
  'libraries': [
    '-Wl,-rpath,@loader_path',
    ## 此外,還可以設(shè)置到任何相對于 .node 文件的其他目錄下
    '-Wl,-rpath,@loader_path/../../darwin/arm64',
  ]
},

但是這也要求 .dylib 文件支持該功能,可以通過 otool -D <你的動態(tài)鏈接庫位置>.dylib 的返回結(jié)果來檢查:

<你的動態(tài)鏈接庫>.dylib:
@rpath/<鏈接庫名稱>.dylib

如果文件名往前的開頭是 @rpath,則意味著支持該功能。如果不是,則可以使用 install_name_tool 來修改動態(tài)鏈接庫使其支持:

install_name_tool -id "@rpath/<鏈接庫名稱>.dylib" <你的動態(tài)鏈接庫位置>.dylib

在 MacOS 下支持 Objective-C 和 C++ 混編

'xcode_settings': {
  'OTHER_CFLAGS': [
    '-ObjC++'
  ]
}

開發(fā)&分發(fā)&使用

項目文件組織

通常來說,我們可以用下面的文件夾結(jié)構(gòu)來扁平地組織我們的 addon 文件:

.
├── node_modules                   ## npm 依賴
├── build                          ## 構(gòu)建路徑
│   ├── Release                   ## Release 產(chǎn)物路徑
│       ├── myaddon.node          ## addon 產(chǎn)物
│       ├── myaddon.node.dSYM     ## addon 的符號文件
├── binding.gyp                    ## 構(gòu)建配置
├── addon.cc                       ## Addon 的 C++ 源碼
├── index.js                       ## Addon 的 JavaScript 源碼
├── index.d.ts                     ## Addon 的 TypeScript 類型(下方會介紹)
└── package.json                   ## Addon 的 package.json 文件

當(dāng)然我們也可以把 JavaScript 源碼和 C++ 源碼分別放入不同的文件夾,只需要修改對應(yīng)的構(gòu)建配置和 package.json 即可。

編寫 index.js - 使用 bindings 包

一般來說我們會直接在 C++ 中實現(xiàn)大部分邏輯,JavaScript 文件只用來引入 .node 文件。由于 Node.js Addon 存在各種不同的方案、構(gòu)建配置,因此 .node 文件產(chǎn)物的位置可能也會因此不同,所以我們需要借助一個第三方 npm 包來自動為我們尋找 .node 文件的位置:

https://github.com/TooTallNate/node-bindings

通過 bindings,我們的 index.js 僅需一行代碼就能自動獲取并導(dǎo)出 .node 模塊:

module.exports = require('bindings')('binding.node')

同時保證 package.json 的 main 配置為我們的 index.js:

{
  // ...
  "main": "index.js"
  // ...
}

為 Addon 添加 TypeScript 類型

添加 TypeScript 類型,最簡單的方式只需要創(chuàng)建一個 index.d.ts 文件,并在其中聲明在 C++ 代碼中創(chuàng)建的函數(shù)們即可:

export interface FooOptions {
  bar: string
}

export function foo(options: FooOptions)

并在 package.json 添加一行參數(shù)用于指向類型文件:

{
  // ...
  "types": "index.d.ts"
  // ...
}

大部分情況下,這個方法就可以給你的 Node.js Addon 聲明類型。

分發(fā)形式

安裝時編譯

一種方式是在使用者進行 npm install 時,使用用戶設(shè)備進行 Addon 的編譯。這時候我們可以使用 install 鉤子來實現(xiàn),我們僅需在 package.json 文件中添加如下內(nèi)容:

{
  // ...
  "scripts": {
    // ...
    "install": "prebuild-install || node-gyp rebuild --release"
    // ...
  }
  // ...
}

保險起見,確保 node-gyp 在你的 devDependencies 之中,這樣就能在用戶通過 npm 安裝你的 Addon 時,自動編譯當(dāng)前系統(tǒng)架構(gòu)所對應(yīng)的產(chǎn)物。

預(yù)編譯

如果希望更近一步,節(jié)約用戶安裝 Addon 的時間,或者是為了讓用戶無需具備編譯環(huán)境即可安裝 Addon,可以使用預(yù)編譯方案。即在集成環(huán)境中提前編譯常見的操作系統(tǒng)、架構(gòu)對應(yīng)的 .node 文件,并隨著 npm 包進行分發(fā),再通過 bindings 或者其他一些庫來自動匹配尋找系統(tǒng)所需要的對應(yīng) .node 文件。

由于預(yù)編譯方案涉及到更多的細節(jié),本文不再做介紹,大家可以參考該項目:

https://github.com/mapbox/node-pre-gyp

責(zé)任編輯:龐桂玉 來源: 字節(jié)跳動技術(shù)團隊
相關(guān)推薦

2021-07-16 04:56:03

NodejsAddon

2014-07-11 14:16:15

AbsurdJSExpress

2020-10-26 08:34:13

Node.jsCORS前端

2013-03-28 14:54:36

2012-02-02 15:14:29

Node.js

2021-12-13 11:21:46

NodePython開發(fā)

2023-08-29 09:43:21

Node.js.env

2011-11-10 11:08:34

Node.js

2021-12-01 00:05:03

Js應(yīng)用Ebpf

2013-11-01 09:34:56

Node.js技術(shù)

2020-01-15 14:20:07

Node.js應(yīng)用程序javascript

2015-03-10 10:59:18

Node.js開發(fā)指南基礎(chǔ)介紹

2019-07-09 14:50:15

Node.js前端工具

2019-02-15 10:49:37

Node.jsweb服務(wù)器

2021-12-25 22:29:57

Node.js 微任務(wù)處理事件循環(huán)

2020-05-29 15:33:28

Node.js框架JavaScript

2012-02-03 09:25:39

Node.js

2024-09-25 08:04:58

2014-02-19 16:28:53

Node.jsWeb工具

2020-07-31 13:35:34

Node.js應(yīng)用分析前端
點贊
收藏

51CTO技術(shù)棧公眾號

91精品天堂福利在线观看 | 伊人免费在线| 成人精品国产亚洲| 粉嫩嫩av羞羞动漫久久久| 亚洲图片欧美日产| 国产精品播放| 少妇视频一区二区| 亚洲第一天堂网| 成人在线国产| 在线视频一区二区三区| 精品不卡在线| 国产精品第九页| 日韩欧国产精品一区综合无码| 久久久美女艺术照精彩视频福利播放| 欧美日韩不卡合集视频| 国产传媒免费观看| 蜜桃视频在线观看www社区| 日韩综合在线视频| 亚洲欧洲中文天堂| 精品人妻一区二区三区四区在线 | 在线播放欧美女士性生活| 人禽交欧美网站免费| 高清乱码免费看污| 国产欧美日韩视频在线| 日韩欧美在线视频观看| 3d动漫精品啪啪一区二区三区免费 | 国产亚洲精品成人| 欧美日韩国产一区二区在线观看| 国产精品美女久久久久久久| 国产精品亚洲自拍| 国产黄a三级三级| 欧美亚洲国产日韩| 91黄色小视频| 色综合视频二区偷拍在线| jizz国产在线| 久久久久久美女精品| 亚洲精品乱码久久久久久按摩观| 国产乱子伦农村叉叉叉| 免费一级毛片在线观看| 日本视频一区二区三区| 最近2019年好看中文字幕视频| 日韩精品aaa| 欧美草逼视频| 91丨porny丨国产| 国产成人精品一区二区| 天堂а√在线中文在线鲁大师| 天天躁日日躁成人字幕aⅴ| 色网站国产精品| 亚欧洲精品在线视频免费观看| 天堂v视频永久在线播放| 久久精品综合| 久久精品福利视频| 日本一区二区免费视频| 成人日韩精品| 亚洲人吸女人奶水| 欧美成人综合一区| 国产精品国产三级国产普通话对白| 你懂的视频一区二区| 亚洲精品久久久久中文字幕欢迎你 | 国产精品日本精品| 疯狂撞击丝袜人妻| 久久99精品国产自在现线| 色老头久久综合| 茄子视频成人免费观看| 日韩av成人| 蜜桃久久精品一区二区| 久久久久久久久久亚洲| 国产三级短视频| 国产精品一区二区三区www| 午夜精品久久久久久久久久久 | 一级做a爱视频| 二区三区精品| 精品久久久久久久久久久久久久久久久| 成人毛片一区二区| 网友自拍视频在线| 亚洲色图在线播放| 91.com在线| av在线收看| av不卡一区二区三区| 成人午夜在线观看| 无码人妻aⅴ一区二区三区有奶水 无码免费一区二区三区 | 欧美最猛黑人xxxxx猛交| 中文字幕av不卡在线| 动漫一区二区| 国产精品电影一区二区三区| 久久久久一区二区三区| 性一交一乱一透一a级| 成人av电影在线| 91日本在线观看| jizz国产在线| 国产精品一级片| 国产精品一久久香蕉国产线看观看 | 欧美激情第8页| 欧美性受xxxx黑人猛交| www日韩在线| 久久99国产精品视频| 精品国产乱码久久久久久图片 | 亚洲一区在线免费| 人人妻人人澡人人爽人人欧美一区| 秋霞电影网一区二区| 成人在线免费观看视视频| 欧美一级做性受免费大片免费| 国产无一区二区| 蜜桃传媒视频麻豆第一区免费观看| 第一页在线观看| 久久精品亚洲一区二区三区浴池| 免费成人进口网站| 成人在线免费看黄| 亚洲欧洲中文日韩久久av乱码| av之家在线观看| 99er精品视频| 欧美一级欧美三级在线观看| 五月花丁香婷婷| 国产第一精品| 日韩精品免费看| av最新在线观看| 久久影院亚洲| 国产91精品在线播放| 黄色片网站在线免费观看| 亚洲一区二区动漫| 欧美与欧洲交xxxx免费观看| 国产日韩欧美视频在线观看| 国产精品一卡二卡在线观看| 欧美激情第六页| 久草视频视频在线播放| 国产亚洲午夜高清国产拍精品| 成年人视频大全| 欧美黄色视屏| 欧美一区三区四区| 伦理片一区二区| 国产精品自在| 精品视频www| a天堂中文字幕| 欧美日韩伦理在线免费| 日韩少妇与小伙激情| 三级av在线免费观看| 日韩高清在线一区| 欧美久久在线| xxxcom在线观看| 日韩欧美不卡一区| 800av在线播放| 国内精品久久久久久久久电影网| 久久久影视精品| 国产99免费视频| 91网站黄www| 欧美日韩黄色一级片| 国内精品免费| 午夜精品久久久久久99热| 日本a级c片免费看三区| 99re这里只有精品6| 亚洲精品一区二区三区樱花 | 欧美激情精品久久久久久免费印度 | 人妻少妇一区二区三区| 亚洲自拍欧美精品| 国产淫片免费看| 国产精品欧美大片| 97视频免费看| 亚洲欧美日本在线观看| 国产精品美女久久久久久久 | 日韩高清在线| 91精品国产综合久久精品性色| 懂色av粉嫩av浪潮av| 欧美日韩国产高清| 国产精品av免费在线观看| 久草在线青青草| 欧美色网站导航| 佐佐木明希电影| 日韩精品一区二区三区免费观影 | 精品一区在线观看视频| 在线观看日韩av电影| 国产精品美乳在线观看| √天堂资源地址在线官网| 性久久久久久久久久久久| 欲求不满的岳中文字幕| 日韩精品五月天| 中文字幕人成一区| 欧美人体一区二区三区| 日韩欧美在线综合网| 国产亚洲精品女人久久久久久| av中文字幕在线不卡| 免费观看成人网| 成人激情自拍| 久久久国产视频| 亚洲精品国产片| 欧美性极品xxxx娇小| 91成人精品一区二区| 国产一本一道久久香蕉| 免费久久99精品国产自| 嫩草伊人久久精品少妇av杨幂| 精品国产欧美一区二区三区成人| 性一交一乱一精一晶| 一本久久精品一区二区| 欧美精品久久久久久久久46p| 成人精品一区二区三区四区| 国产又粗又爽又黄的视频| 波多野结衣在线一区二区 | 亚洲人成电影网| 亚洲天堂狠狠干| 成人免费观看视频| 亚洲视频在线观看一区二区三区| 日本不卡一二三| 精品激情国产视频| 午夜福利理论片在线观看| 欧美老女人第四色| 中文字幕人妻一区二区三区在线视频| 亚洲美女视频在线免费观看| 99在线热播| av在线app| 亚洲欧美日韩中文视频| 中文字幕黄色片| 亚洲欧美一区二区不卡| 亚洲成人黄色av| 成人av资源在线| 国产黑丝在线视频| 欧美在线二区| 亚洲精品视频一区二区三区| 国产精东传媒成人av电影| 成人网在线视频| 成人在线中文| 国产精品av网站| 天堂中文av在线资源库| 亚洲另类图片色| www日本高清| 亚洲国产精品精华液网站| 日本护士做爰视频| 国产尤物一区二区| 天天爽人人爽夜夜爽| 四季av一区二区三区免费观看| 国产精品自拍网| 成人欧美一区二区三区的电影| 亚洲人成在线观看| 天天操天天操天天| 91久久精品一区二区| 日产欧产va高清| 国产日产欧美一区二区三区| 欧美bbbbb性bbbbb视频| 日本在线播放一区二区三区| 99热在线这里只有精品| 亚洲成人在线| 免费人成在线观看视频播放| 日韩三级av| 精品国产福利| 久久资源综合| 黄色99视频| 日韩大尺度在线观看| 精品国产一区二区三区免费| 成人福利免费在线观看| 国产欧美丝袜| 国产精品伊人| 国模精品系列视频| 国产后进白嫩翘臀在线观看视频| 欧美精品免费看| 精华区一区二区三区| 亚洲女人天堂网| 国产一区二区三区不卡在线| 亚洲天堂av在线播放| a天堂在线资源| 色婷婷久久一区二区| 黄网站在线免费| 亚洲天堂久久av| 国产爆初菊在线观看免费视频网站| 亚洲美女性视频| 韩国精品视频| 日韩在线播放视频| 超碰个人在线| 久久久久久18| 亚洲伊人av| 国产精品亚洲网站| 亚洲小说春色综合另类电影| 国产精品久久久久久久久借妻| 牛牛电影国产一区二区| 韩国19禁主播vip福利视频| 极品视频在线| 久久伊人精品视频| 国产午夜视频在线观看| 中文字幕亚洲激情| 免费理论片在线观看播放老| 亚洲一级黄色片| 国产秀色在线www免费观看| 欧美极品少妇xxxxⅹ裸体艺术 | 亚洲国产aⅴ天堂久久| √资源天堂中文在线| 欧美男女性生活在线直播观看| 亚洲av无码一区二区三区dv| 精品中文视频在线| 免费黄色在线观看| 国语自产在线不卡| 成人在线观看免费播放| 国产精品免费一区二区三区四区| 激情中国色综合| 97se国产在线视频| 国产一区二区三区四区大秀| 男人天堂成人网| 香蕉久久夜色精品| 日韩av三级在线| 免费欧美在线视频| 黄色激情在线观看| 国产精品午夜春色av| 免费福利视频网站| 伊人一区二区三区| 综合五月激情网| 欧美性猛xxx| 国产精品一二三四五区| 亚洲区一区二区| 在线观看免费成人av| 欧美在线观看天堂一区二区三区| 九一国产精品视频| 久久福利资源站| 欧美性久久久久| 国产主播一区二区三区| 一卡二卡三卡四卡| 91啦中文在线观看| 91麻豆精品成人一区二区| 色综合久久中文综合久久牛| 国产激情视频在线播放| 日韩一级免费观看| 美国成人毛片| 午夜精品久久久久久久男人的天堂 | 日韩视频免费在线播放| a在线播放不卡| 免费中文字幕视频| 亚洲最新视频在线观看| a片在线免费观看| 欧美日韩国产色站一区二区三区| 亚洲网站免费观看| 亚洲精品少妇网址| 99riav视频在线观看| 日本精品视频在线观看| 福利片一区二区| 国内外成人激情免费视频| 久久er精品视频| 女人黄色一级片| 91国产免费观看| 秋霞av鲁丝片一区二区| 九九九久久国产免费| 国产精品1区| 浴室偷拍美女洗澡456在线| 久久超碰97人人做人人爱| 手机看片日韩av| 欧美一a一片一级一片| 国产精品国产精品国产专区| 在线日韩第一页| 亚洲丝袜一区| 欧洲中文字幕国产精品| 欧美亚洲人成在线| 天堂精品一区二区三区| 丝袜诱惑亚洲看片| www.黄色在线| 欧美日韩日日摸| 日本视频在线播放| 成人在线国产精品| 午夜精品久久| 成年人小视频在线观看| 午夜亚洲国产au精品一区二区| 懂色av蜜臀av粉嫩av分享吧| 亚洲欧美日韩天堂一区二区| 日本乱码一区二区三区不卡| 成人在线国产精品| 91精品婷婷色在线观看| 先锋资源在线视频| 亚洲一区二区三区四区的| 成人免费视频国产免费| 伊人av综合网| 自拍偷拍亚洲| 国产青草视频在线观看| 不卡高清视频专区| 国产又粗又爽视频| 综合国产在线视频| 日韩欧美高清一区二区三区| 日韩资源av在线| 欧美日韩一卡| 亚洲久久久久久| 色美美综合视频| 成人高清免费在线| 国产日韩精品推荐| 久久精品毛片| 国产性生活大片| 亚洲国产美女久久久久| 中文.日本.精品| 警花观音坐莲激情销魂小说 | 国产高清一区二区三区| 亚洲欧美视频| 可以免费看av的网址| 欧美大片一区二区| 免费观看亚洲| 中文字幕av久久| 理论片日本一区| 国产成人福利在线| 欧美一级一级性生活免费录像| caoporn-草棚在线视频最| 亚洲国产一区二区三区在线播| 国产伦精一区二区三区| 天天操天天操天天操天天| 精品国偷自产在线视频99| 婷婷精品在线| 一级日本黄色片| 欧美亚洲高清一区| а√天堂中文在线资源8| 亚洲激情啪啪| 99久久精品免费看|