最近公司項目須要,讓我作一個 node 的插件,目的是把一個視頻播放器接入到 electron 環境。我真是太南了,哪裏須要哪裏搬😳,不吐苦水了。
這篇文章覆蓋了 Windows 設備上配置 Node C++ addon 插件開發環境的全過程。javascript
Node 插件 是用 C/C++ 語言編寫的動態連接庫,使用 require()
加載到 Node 環境中,可以像普通 Node 模塊同樣使用。Node 插件能夠用於編寫高性能 C++ 算法,也能夠用在Node 環境與其餘 C/C++ 庫之間提供接口封裝,實現互相調用。
在早期的 Node 插件開發中,嚴重依賴 V8 引擎的 API,可能都遇到過升級 Node 版本後插件不可用的狀況,須要從新編譯。這是由於 Node 版本升級,V8 引擎的二進制 ABI 接口發生變化,致使以前編譯的 Node 插件不可用。
爲了解決這一問題,在 Node 8.0 版本中發佈了新的 N-API 接口。 N-API 並非一種新的插件編寫模式,N-API 是對 V8 引擎 API 的封裝,以 C 風格 API 提供對外接口,而且保證接口是 ABI 穩定的。使用 N-API 編寫的 Node 插件可以一次編寫、一次編譯,跨多個Node 版本運行。N-API 接口在 8.12.0 以及更高版本中已經處於穩定狀態(參見 abi-stable-node),能夠放心在生產環境投入使用。html
這裏我安裝的是 Visual Studio 2017 社區版,安裝的時候選上 C++ 開發組件。
java
在 Python 官網下載二進制安裝包便可。因爲我後面要對接的 C++ 庫是 x86 架構的,爲了不可能出現的麻煩,這裏 Python 我選擇的也是 x86 架構安裝包。node
node 版本那麼多,我該用哪一個呢?要是後面切換怎麼辦? 都不怕,nvm 都搞定。
在 github.com/coreybutler… 下載 windwos nvm 安裝包進行 nvm 安裝。
在 cmd 命令行中輸入 nvm install 10.17.0 32
安裝 node 長期支持版 10.17.0 x86 架構版本,
而後輸入 nvm use 10.17.0 32
激活使用對應的 node 版本。react
npm install -g node-gyp
複製代碼
編譯 Node 插件使用 node-gyp。如今有了 node-gyp 確實方便了不少,不用像早期開發插件要在不一樣地方配置各類讓人頭疼的編譯、連接參數。webpack
在 github.com/nodejs/node… 中有官方提供的多個 Node 插件上手示例項目。其中多數小 demo 官方有提供了 3 種實現方式,分別是 NAN ,N-API 以及 node-addon-api。node-addon-api 是對 C 形式的 N-API 的 C++ 封裝,一樣是 ABI 兼容的。我我的推薦使用 node-addon-api。NAN 是早期的寫插件使用的 API,須要和 V8 API 結合使用,如今已經再也不推薦。git
下載並打開 node-addon-examples 中的 1_hello_world,使用本身最順手的編輯器 Visual Studio Code 打開文件夾。在 VSCode 的集成終端中, cd node-addon-api && npm install && node-gyp build && npm test
一鼓作氣,就能夠看到 Node 插件版 hello world 的輸出結果了。github
在 node-gyp build 命令以後,會在 build 文件夾中生成 Visual Studio 工程文件 binding.sln,用 Visual Studio 2017 打開,可使用智能代碼提示和補全功能加快 Node 插件的編寫,在 Visual Studio 中也能夠編譯插件項目。
實際上使用 node-gyp configure
命令就會生成 Visual Studio 工程文件。web
經過使用 node-addon-api,插件代碼比直接使用 N-API 更加簡潔、易讀。
NODE_API_MODULE 第一個參數是插件名稱,第二個參數是 Init 註冊函數。Init 註冊函數中,將 hello
綁定到函數 Method
上。算法
#include <napi.h>
Napi::String Method(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "world");
}
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)
複製代碼
在 javascript 端,可使用 bindings 來加載模塊。由於 Node 插件歷史發展中,二進制文件會被編譯產出到不少不一樣的位置,使用 bindings 能夠解決尋找插件路徑的問題,bindings 檢查全部可能的插件構建位置,返回第一個成功的加載位置。
const addon = require('bindings')('hello');
console.log(addon.hello());
複製代碼
使用 node 運行此 js 文件,成功輸出 world
簡單的 demo 中咱們已經完成 Node 插件的編寫、編譯、加載、運行所有環節了。下一步就是把這些搬到 Electron 開發運行環境中。
先來兩個提醒:
一、npm install electron 必定要確認安裝成功。
否則出現錯誤讓人摸不着頭腦。好比我就遇到這個錯誤。
Error: Electron failed to install correctly, please delete node_modules/electron and try installing again.
以及這個錯誤
electron@7.0.0 postinstall D:\electron\electron-react\node_modules\electron
> node install.js
(node:12492) UnhandledPromiseRejectionWarning: Error: EPERM: operation not permitted, lstat 'C:\Users\befovy\AppData\Local\Temp\electron-download-Jj9PbA\electron-v7.0.0-win32-ia32.zip'
(node:12492) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
解決上面錯誤讓我很崩潰。錯誤提示看起來是權限問題,我是用管理員權限的 cmd 依然出錯。github 上有人說 npm cache clean
能夠解決,可是也不行。最後我試了新建一個電腦用戶,重啓等大法,居然莫名其妙好了。
二、使用的 Node 版本必定要各 Electron 支持的 Node 版本匹配,
在 Electron 版本頁面 electronjs.org/releases/st… 能夠查看 Electron 匹配的 Node 版本。此頁面上說明指出,Electron 7.0.0 版本匹配的 Node 版本是 12.x 。全部我用 nvm 將 Node 版本切換到 12.13.0 後,Electron 才成功運行起來。
從 github.com/electron/el… clone 一個 electron starter,本文中 clone 此項目時 git 提交記錄是 77d1cb4
。 按照本文以前的內容設置 node 版本 12.13.0 32, 而後 npm install
, npm start
electron App 就成功運行起來了。
對 Electron 有必定了解的話就會知道 Electron 中有主進程和 renderer 進程。本文中咱們在這兩個進程中都會嘗試調用 addon 中的函數。
const addon = require('bindings')('hello');
module.exports = {
addon: addon
}
複製代碼
"dependencies": {
"hello_world": "file:../hello"
}
複製代碼
修改 package.json,以相對目錄形式引入依賴。
// main.js
function createWindow () {
//... ... 省略上下文代碼
mainWindow.loadFile('index.html')
const {addon} = require('hello_world')
console.log(`hello ${addon.hello()} from main.js`)
//... ... 省略上下文代碼
}
// preload.js
window.addEventListener('DOMContentLoaded', () => {
//... ... 省略上下文代碼
const {addon} = require('hello_world')
replaceText(`hello-world`, addon.hello())
}
// renderer.js
const {addon} = require('hello_world');
console.log(`hello ${addon.hello()} from renderer.js`)
複製代碼
// index.html
<p> hello <span id="hello-world"></span> from preload.js. </p>
複製代碼
加上上面的代碼修改以後, npm start
打開 electron app,能夠看到 main.js 和 preload.js 中對於 addon 插件函數的調用成功執行。可是 renderer.js 中調用出錯,出錯緣由是 require
未定義
app.on('ready', () => {
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true
}
});
});
複製代碼
修改以後,renderer.js 中也成功調用到 addon 中的函數
文中使用到的 Node addon 以及項目配置代碼都在 github 上 github.com/befovy/node…。
此提交和文中內容徹底一致 ,更新日期爲 2019年10月27日。
原本還計劃在一下 Electron 和 React 結合的環境中加載使用 Node 插件,奈何短期內沒搞定。留着之後在搞吧。
在 Electron 和 React 結合的項目中,main.js 和 preload.js 中均可以跟單獨 Electron 環境同樣使用 Node 插件。可是 React 的相關 js 代碼中就不行,問題還比較複雜,目前判斷是由於 webpack 打包不太理解 require('hello.node')
,可是因爲不熟悉 webpack,因此這個沒搞定。
我還會回來的!!
如下內容於 2019年10月28日 更新。
Electron 和 React 結合的環境中成功在 main.js 、 preload.js 以及 React 代碼 App.js 等地方成功調用 addon 中的函數。
通過和同事討論,preload.js 是和 App.js 等在同一個進程中執行,能夠傳遞參數。
修改代碼以下:
// preload.js
global.addon = require('hello_world')
複製代碼
// App.js
const {addon} = window.addon;
function App() {
return (
<p>Learn React, hello {addon.hello()} </p>
);
}
複製代碼
到這裏算是真的大功告成了。
全部代碼在前文中 github 倉庫查看,commit hash 是 b841f0d。