阅读 1444

Node C++ 插件 Windows Electron 环境配置

最近公司项目需要,让我做一个 node 的插件,目的是把一个视频播放器接入到 electron 环境。我真是太南了,哪里需要哪里搬😳,不吐苦水了。
这篇文章覆盖了 Windows 设备上配置 Node C++ addon 插件开发环境的全过程。

啥是 Node C++ 插件

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),可以放心在生产环境投入使用。

环境准备

  • 安装 Visual Studio 2017

这里我安装的是 Visual Studio 2017 社区版,安装的时候选上 C++ 开发组件。

  • 安装 Python 2.7

在 Python 官网下载二进制安装包即可。由于我后面要对接的 C++ 库是 x86 架构的,为了避免可能出现的麻烦,这里 Python 我选择的也是 x86 架构安装包。

  • 安装 nvm、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 版本。

  • 安装 node-gyp
npm install -g node-gyp
复制代码

编译 Node 插件使用 node-gyp。现在有了 node-gyp 确实方便了很多,不用像早期开发插件要在不同地方配置各种让人头疼的编译、链接参数。

Hello World

在 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 结合使用,现在已经不再推荐。

下载并打开 node-addon-examples 中的 1_hello_world,使用自己最顺手的编辑器 Visual Studio Code 打开文件夹。在 VSCode 的集成终端中, cd node-addon-api && npm install && node-gyp build && npm test  一气呵成,就可以看到 Node 插件版 hello world 的输出结果了。

在 node-gyp build 命令之后,会在 build 文件夹中生成 Visual Studio 工程文件 binding.sln,用 Visual Studio 2017 打开,可以使用智能代码提示和补全功能加快 Node 插件的编写,在 Visual Studio 中也可以编译插件项目。
实际上使用 node-gyp configure 命令就会生成 Visual Studio 工程文件。

通过使用 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

在 Electron 项目中使用

简单的 demo 中我们已经完成 Node 插件的编写、编译、加载、运行全部环节了。下一步就是把这些搬到 Electron 开发运行环境中。

先来两个提醒:

1、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 可以解决,但是也不行。最后我试了新建一个电脑用户,重启等大法,竟然莫名其妙好了。

2、使用的 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 中的函数。

  • 首先在 Node 插件目录中 index.js 文件进行符号导出。
const addon = require('bindings')('hello');
module.exports = {
    addon: addon
}
复制代码
  • 在 Electron 中通过 npm 引入对 addon 的依赖
  "dependencies": {
    "hello_world": "file:../hello"
  }
复制代码

修改 package.json,以相对目录形式引入依赖。

  • 在 main.js 、preload.js、renderer.js 中调用 addon 插件的函数
// 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  未定义

image.png



网上查找一番原因,在 Stack Overflow 找到了解决办法。

app.on('ready', () => {
    mainWindow = new BrowserWindow({
        webPreferences: {
            nodeIntegration: true
        }
    });
});
复制代码

修改之后,renderer.js 中也成功调用到 addon 中的函数

image.png


尚未结束

文中使用到的 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,所以这个没搞定。
我还会回来的!!

我又回来了 Electron + React

以下内容于 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