最近公司项目须要,让我作一个 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。