- March 28, 2018
Node.js 原生模組是用 C++ 編寫的 Node.js 擴充套件。C++ 原始碼通過 node-gyp 編譯為 .node 字尾的二進位制檔案(類似於 .dll 和 .so)。在 Node.js 環境中可以直接用 require() 函式將 .node 檔案初始化為動態連結庫。一些 npm 包會包含 C++ 擴充套件,例如: node-ffi、node-iconv、node-usb,但都是原始碼版本,在安裝後需要編譯後才能被 Node.js 呼叫。
Electron 同樣也支援 Node 原生模組,但由於和官方的 Node 相比使用了不同的 V8 引擎,如果你想編譯原生模組,則需要手動設定 Electron 的 headers 的位置。
環境準備
不管是 Node.js 環境或是 Electron 中使用原生模組,都需要準備一個編譯工具 node-gyp。我們這裡使用的是 windows 環境開發,參考 node-gyp 的安裝說明還需要安裝 windows-build-tools。
用管理員許可權開啟 CMD 或 PowerShell 視窗,執行以下命令:
npm i -g node-gyp
npm i -g --production windows-build-tools複製程式碼
windows-build-tools 安裝時間可能會長一點,要耐心等待。
專案配置
在安裝 npm 模組之前還要設定一些環境變數,建議在專案目錄下放一個 .npmrc
檔案,內容如下:
registry=https://registry.npm.taobao.org
NODEJS_ORG_MIRROR=https://npm.taobao.org/mirrors/node
ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron
arch=ia32
target_arch=ia32
msvs_version=2015
disturl=https://atom.io/download/electron
runtime=electron
target=1.8.4
build_from_source=true複製程式碼
引數說明:
registry - 配置 npm 包映象
NODEJS_ORG_MIRROR - 配置 Node.js 標頭檔案下載映象
ELECTRON_MIRROR - Electron 下載映象
disturl - Electron 標頭檔案映象(用了 electron-rebuild 模組才需要)
arch、target_arch - 根據目標環境定義為 ia32 或 x64
編譯
通過以上配置不需要 electron-rebuild ,直接用 npm 或 yarn 安裝新原生包的時候,會自動編譯為適用當前 electron 版本的原生模組到 {module_name}/build/Release/xxx.node
。由於這個構建後的路徑是動態的,node-ffi
等第三方模組會使用 bindings 去動態找到這個 .node 檔案。bindings 的原理是,首先定位到當前包的目錄,然後通過預設一些搜尋路徑,一個個嘗試讀取,直到找到為止。
專案構建
問題:找不到 .node 檔案
bindings 的搜尋方式在 js 原始碼未壓縮的情況下當然沒問題,但我們的專案中通常還使用了 webpack。
在開發模式下能夠找到 node_modules 下的檔案也沒有問題,構建到生產環境後就沒有 node_modules 了,而且 webpack 也不支援打包動態路徑的檔案。我想到兩種解決方案:
方案一、將 node-ffi 拷貝一份修改 bindings 為寫死路徑,當然每個用到 bindings 的包都要修改。
// ffi 下的 bindings.js
module.exports = require('bindings')('ffi_bindings.node')
// 改為
module.exports = require('../build/Release/ffi_bindings.node')複製程式碼
方案二、自己實現一個 bindings 對映,並利用 webpack 的 alias 功能替換 bindings 模組。
- 增加 bindings.js
function bindings (name) {
if (name === 'ffi_bindings.node') {
return require('ffi/build/Release/ffi_bindings.node')
}
if (name === 'binding') {
return require('ref/build/Release/binding.node')
}
}
module.exports = bindings複製程式碼
- 配置 webpack 別名
// webpack.main.config.js
module.exports = {
...,
resolve: {
extensions: ['.js', '.json', '.node'],
alias: {
// 如果用 bindings 包,就會找不到 .node 模組,這裡替換成自己的實現
'bindings': path.resolve(__dirname, '../addons/bindings.js')
}
},
...複製程式碼
顯然方案二更好一點,只需要自己實現一個 bindings.js,而不用去動第三方包的原始碼,所以我們直接用了方案二。
問題:打包 asar 後,提取出 .node 檔案
Electron 官方文件應用程式打包有說明,二進位制檔案不要在 asar 中執行,需要 unpack 出來。我們用了 electron-packager 可以通過 asar 引數配置:
asar: {
unpack: '*.node'
}複製程式碼
這樣,會把 .node 檔案都提取到 app.asar.unpacked
目錄。但是它只負責提取並不會自動更新 .node 檔案的訪問地址到新的路徑。所以我想到了用 webpack 的 file-loader
:
// webpack.main.config.js
module: {
rules: [
{
test: /\.node$/,
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'addons',
publicPath: '../../app.asar.unpacked/addons'
}
}
]
}複製程式碼
我 app 打包後的專案結構是這樣的
app.asar.unpacked
app
---main
---renderer
---package.json複製程式碼
JS 是在 main 目錄的主執行緒裡面訪問原生模組的,打包出來的路徑也是這樣。所以相對路徑是要向上兩級找到 app.asar.unpacked 目錄。
執行一下 npm run build
打包。
檔案按我預期那樣生成好了!
好的,執行一下程式:
TypeError: Cannot read property 'int64' of undefined
程式跑不起來了!
bindings 也是直接 require('addon.node') 呀,Node.js 官網也是這樣說的。怎麼構建後就不行了呢?
後面,我在webpack 的 loader 中找到 node-loader,裡面說明
在 enhanced-require 中執行 node add-ons
所以,node-loader 是針對魔改過的require
(非 node 環境 require)的。這有可能是在 Electron 或是 webpack 發生的。
於是我對.node
檔案增加一個node-loader
module: {
rules: [
{
test: /\.node$/,
use: [
'node-loader',
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'addons',
publicPath: '../../app.asar.unpacked/addons'
}
}
]
}
]
}複製程式碼
然後重新構建。
一切問題都解決了!
相關資料:
electronjs.org/docs/tutori…
nodejs.org/dist/latest…
github.com/nodejs/node…
doc.webpack-china.org/loaders/nod…