node 是由 c++ 編寫的,核心的 node 模塊也都是由 c++ 代碼來實現,因此一樣 node 也開放了讓使用者編寫 c++ 擴展來實現一些操做的窗口。
若是你們對於 require 函數的描述還有印象的話,就會記得若是不寫文件後綴,它是有一個特定的匹配規則的:
LOAD_AS_FILE(X) 1. If X is a file, load X as its file extension format. STOP 2. If X.js is a file, load X.js as JavaScript text. STOP 3. If X.json is a file, parse X.json to a JavaScript Object. STOP 4. If X.node is a file, load X.node as binary addon. STOP
能夠看到,最後會匹配一個 .node
,然後邊的描述也表示該後綴的文件爲一個二進制的資源。
而這個 .node
文件通常就會是咱們所編譯好的 c++ 擴展了。javascript
能夠簡單理解爲,若是想基於 node 寫一些代碼,作一些事情,那麼有這麼幾種選擇:前端
平常的開發其實只用第一項就夠了,咱們用本身熟悉的語言,寫一段熟悉的代碼,而後發佈在 NPM 之類的平臺上,其餘有相同需求的人就能夠下載咱們上傳的包,而後在TA的項目中使用。
但有的時候可能純粹寫 JS 知足不了咱們的需求,也許是工期趕不上,也許是執行效率不讓人滿意,也有多是語言限制。
因此咱們會採用直接編寫一些 c++ 代碼,來建立一個 c++ 擴展讓 node 來加載並執行。
何況若是已經有了 c++ 版本的輪子,咱們經過擴展的方式來調用執行而不是本身從頭實現一套,也是避免重複造輪子的方法。 java
一個簡單的例子,若是你們接觸過 webpack 而且用過 sass 的話,那麼在安裝的過程當中極可能會遇到各類各樣的報錯問題,也許會看到 gyp 的關鍵字,其實緣由就是 sass 內部有使用一些 c++ 擴展來輔助完成一些操做,而 gyp 就是用來編譯 c++ 擴展的一種工具。 node
https://github.com/sass/node-sasswebpack
固然,上邊也提到了還有第三種操做方法,咱們能夠直接魔改 node 源碼,可是若是你只是想要寫一些原生 JS 實現起來沒有那麼美好的模塊,那麼是沒有必要去魔改源碼的,畢竟改完了之後還要編譯,若是其餘人須要用你的邏輯,還須要安裝你所編譯好的特殊版本。
這樣的操做時很不易於傳播的,你們不會想使用 sass 就須要安裝一個 sass 版本的 node 吧。
就像爲了看星戰還要專門下載一個優酷- -。 c++
簡單總結一下,寫 c++ 的擴展大概有這麼幾個好處:git
node 從問世到如今已經走過了 11 年,經過早期的資料、博客等各類信息渠道能夠看到以前開發一個 c++ 擴展並非很容易,但通過了這麼些年迭代,各類大佬們的努力,咱們再去編寫一個 c++ 擴展已是比較輕鬆的事情了。
這裏直入正題,放出今天比較關鍵的一個工具:node-addon-api module
以及這裏是官方提供的各類簡單 demo 來讓你們熟悉這是一個什麼樣的工具: node-addon-examples github
須要注意的一點是, demo 目錄下會分爲三個子目錄,在 readme 中也有寫,分別是三種不一樣的 c++ 擴展的寫法(基於不一樣的工具)。
咱們本次介紹的是在 node-addon-api
目錄下的,算是三種裏邊最爲易用的一種了。 web
首先是咱們比較熟悉的 package.json
文件,咱們須要依賴兩個組件來完成開發,分別是 bindings 和 node-addon-api。 算法
而後咱們還須要簡單瞭解一下 gyp 的用法,由於編譯一個 c++ 擴展須要用到它。
就像 helloworld 示例中的 binding.gyp 文件示例:
{ "targets": [ { // 導出的文件名 "target_name": "hello", // 編譯標識的定義 禁用異常機制(注意感嘆號表示排除過濾) "cflags!": [ "-fno-exceptions" ], // c++ 編譯標識的定義 禁用異常機制(注意感嘆號表示排除過濾,也就是 c++ 編譯器會去除該標識) "cflags_cc!": [ "-fno-exceptions" ], // 源碼入口文件 "sources": [ "hello.cc" ], // 源碼包含的目錄 "include_dirs": [ // 這裏表示一段 shell 的運行,用來獲取 node-addon-api 的一些參數,有興趣的老鐵能夠自行 node -p "require('node-addon-api').include" 來看效果 "<!@(node -p \"require('node-addon-api').include\")" ], // 環境變量的定義 'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], } ] }
gyp 的語法挺多的,此次並非單獨針對 gyp 的一次記錄,因此就不過多的介紹。
而後咱們來實現一個簡單的建立一個函數,讓兩個參數相加,並返回結果。
源碼位置: https://github.com/Jiasm/node...
咱們須要這樣的一個 binding.gyp 文件:
{ "targets": [ { "target_name": "add", "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], "sources": [ "add.cc" ], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], 'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ], } ] }
而後咱們在項目根目錄建立 package.json 文件,並安裝 bindings 和 node-addon-api 兩個依賴。
接下來就是去編寫咱們的 c++ 代碼了:
#include <napi.h> // 定義 Add 函數 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; } // 入口函數,用於註冊咱們的函數、對象等等 Napi::Object Init(Napi::Env env, Napi::Object exports) { // 將一個名爲 add 的函數掛載到 exports 上 exports.Set(Napi::String::New(env, "add"), Napi::Function::New(env, Add)); return exports; } // 固定的宏使用 NODE_API_MODULE(addon, Init)
在 c++ 代碼完成之後就是須要用到 node-gyp
的時候了,建議全局安裝 node-gyp
,避免一個項目中出現多個 node_modules 目錄的時候使用 npx
會出現一些不可預料的問題:
> npm i -g node-gyp # 生成構建文件 > node-gyp configure # 構建 > node-gyp build
這時候你會發現項目目錄下已經生成了一個名爲 add.node 的文件,就是咱們在 binding.gyp 裏邊的 target_name 所設置的值了。
最後咱們就是要寫一段 JS 代碼來調用所生成的 .node 文件了:
const { add } = require('bindings')('add.node') console.log(add(1, 2)) // 3 console.log(add(0.1, 0.2)) // 熟悉的 0.3XXXXX
接下來咱們來整點好玩的,實現一個前端的高頻考題,如何實現一個函數柯里化,定義以下:
add(1)(2)(3) // => 6 add(1, 2, 3) // => 6
源碼位置: https://github.com/Jiasm/node...
咱們會用到的一些技術點:
再也不贅述 binding.gyp 與 package.json 的配置,咱們直接上 c++ 代碼:
#include <napi.h> // 用來覆蓋 valueOf 實現的函數 Napi::Value GetValue(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); // 獲取咱們在建立 valueOf 函數的時候傳入的 result double* storageData = reinterpret_cast<double*>(info.Data()); // 避免空指針狀況 if (storageData == NULL) { return Napi::Number::New(env, 0); } else { return Napi::Number::New(env, *storageData); } } Napi::Function CurryAdd(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); // 獲取咱們下邊在建立 curryAdd 函數的時候傳入的 result double* storageData = reinterpret_cast<double*>(info.Data()); double* result = new double; // 遍歷傳入的全部參數 long len, index; for (len = info.Length(), index = 0; index < len; index++) { double arg = info[index].As<Napi::Number>().DoubleValue(); *result += arg; } // 用於屢次的計算 if (storageData != NULL) { *result += *storageData; } // 建立一個新的函數用於函數的返回值 Napi::Function fn = Napi::Function::New(env, CurryAdd, "curryAdd", result); // 篡改 valueOf 方法,用於輸出結果 fn.Set("valueOf", Napi::Function::New(env, GetValue, "valueOf", result)); return fn; } Napi::Object Init(Napi::Env env, Napi::Object exports) { Napi::Function fn = Napi::Function::New(env, CurryAdd, "curryAdd"); exports.Set(Napi::String::New(env, "curryAdd"), fn); return exports; } NODE_API_MODULE(curryadd, Init)
編譯完成之後,再寫一段簡單的 JS 代碼來調用驗證結果便可:
const { curryAdd } = require('bindings')('curry-add'); const fn = curryAdd(1, 2, 3); const fn2 = fn(4); console.log(fn.valueOf()) // => 6 console.log(fn2.valueOf()) // => 10 console.log(fn2(5).valueOf()) // => 15
而後能夠講一下上邊列出來的三個技術點是如何解決的:
如何在 c++ 函數中返回一個函數供 JS 調用
Napi::Function::New
建立新的函數,並將計算結果存入函數能夠獲取到的地方供下次使用如何讓返回值既支持函數調用又支持取值操做
fn.Set
篡改 valueOf
函數並返回結果如何處理非固定數量的參數(其實這個很簡單了,從上邊也能看出來,自己就是一個數組)
info
的 Length
來遍歷獲取固然,就例如柯里化之類的函數,拿JS來實現的話會很是簡單,配合 reduce 函數基本上五行之內就能夠寫出來。
那咱們折騰這麼多到底是爲了什麼呢?
這就要回到開頭所說的優點了: 執行效率
爲了證實效率的差別,咱們選擇用一個排序算法來驗證,採用了最簡單易懂的冒泡排序來作,首先是 JS 版本的:
源碼位置: https://github.com/Jiasm/node...
function bubble (arr) { for (let i = 0, len = arr.length; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] < arr[j]) { [arr[i], arr[j]] = [arr[j], arr[i]] } } } return arr } bubble([7, 2, 1, 5, 3, 4])
而後是咱們的 c++ 版本,由於是一個 JS 的擴展,因此會涉及到數據類型轉換的問題,大體代碼以下:
#include <napi.h> void bubbleSort(double* arr, int len) { double temp; int i, j; for (i = 0; i < len; i++) { for (j = i + 1; j < len; j++) { if (*(arr + i) < *(arr + j)) { temp = *(arr + i); *(arr + i) = *(arr + j); *(arr + j) = temp; } } } } Napi::Value Add(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); Napi::Array array = info[0].As<Napi::Array>(); int len = array.Length(), i; // 返回值 Napi::Array arr = Napi::Array::New(env, len); double* list = new double[len]; // 將 Array 轉換爲 c++ 可方便使用的 double 數組 for (i = 0; i < len; i++) { Napi::Value i_v = array[i]; list[i] = i_v.ToNumber().DoubleValue(); } // 執行排序 bubbleSort(list, len); // 將 double 數組轉換爲要傳遞給 JS 的數據類型 for (i = 0; i < len; i++) { arr[i] = Napi::Number::New(env, list[i]); } return arr; } Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set(Napi::String::New(env, "bubble"), Napi::Function::New(env, Add)); return exports; } NODE_API_MODULE(bubble, Init)
而後咱們經過一個隨機生成的數組來對比耗時:
const { bubble } = require('bindings')('bubble.node') const arr = Array.from(new Array(1e3), () => Math.random() * 1e6 | 0) console.time('c++') const a = bubble(arr) console.timeEnd('c++') function bubbleJS (arr) { for (let i = 0, len = arr.length; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] < arr[j]) { [arr[i], arr[j]] = [arr[j], arr[i]] } } } return arr } console.time('js') bubbleJS(arr) console.timeEnd('js')
在 1,000
數據量的時候耗時差距大概在 6
倍左右,在 10,000
數據量的時候耗時差距大概在 3
倍左右。
也是簡單的證明了在相同算法狀況下 c++ 效率確實是會比 JS 高一些。
固然了,也經過上邊的 bubble sort 能夠來證明另外一個觀點: 有更多的 c++ 版本的輪子能夠拿來用
就好比上邊的 bubbleSort
函數,可能就是一個其餘的加密算法實現、SDK 封裝,若是沒有 node 版本,而咱們要使用就須要參考它的邏輯從新實現一遍,但若是採用 c++ 擴展的方式,徹底能夠基於原有的 c++ 函數進行一次簡單的封裝就擁有了一個 node 版本的 函數/SDK。
上邊的一些內容就是如何使用 node-addon-api
來快速開發一個 c++ 擴展,以及如何使用 node-gyp
進行編譯,還有最後的如何使用 JS 調用 c++ 擴展。
在開發 node 程序的過程當中,若是可以適當的利用 c++ 的能力是會對項目有很大的幫助的,在一些比較關鍵的地方,亦或者 node 弱項的地方,使用更鋒利的 c++ 來幫助咱們解決問題。
不要讓編程語言限制了你的想象力