若是咱们的应用想要实现这样一个需求:监听电脑的usb接口,当有新的设备(移动硬盘或者U盘)接入电脑时,可以获取里面的移动设备的状况并更新到应用程序的界面上。javascript
按照 Electron 或者 Node.js 现成的接口,咱们没法直接实现。java
这时候,咱们就能够根据咱们本身的状况,对系统的底层接口封装成独立的动态连接库(.dll),而后把动态连接库暴露给Electron 或者 Node.js 进行调用,从而实现需求。node
Node.js 能够经过对接 .dll,调用系统底层接口从而实现原有接口不提供的功能。同理,Electron 是基于Node.jss 进行封装的,Electron 也就拥有了对接动态连接库,可以把不可能变为可能。c++
截至本文完成时间(2018-12-17)git
现有的主流的方案一共有两项github
自定义C++ Addonsnpm
node-ffi 对接方案bash
(暂不讨论 .net core 等对接方式)数据结构
首先,明确一个观点,Electron 是基于 Node.js 封装的框架,Electron 对接 动态连接库 的底层方案,是基Node.js c++ Addons机制多线程
从 Node.js 官网上能够了解到,Addons机制涉及多个组件和 API 的知识:
V8: Node.js 目前用于提供 JavaScript 实现的 C++ 库。 V8 提供了用于建立对象、调用函数等的机制。 V8 的 API 文档主要在v8.h
头文件中(Node.js 源代码中的deps/v8/include/v8.h
),也能够在查看V8 在线文档。
libuv: 实现了 Node.js 的事件循环、工做线程、以及平台全部的的异步操做的 C 库。 它也是一个跨平台的抽象库,使全部主流操做系统中能够像 POSIX 同样访问经常使用的系统任务,好比与文件系统、socket、定时器、以及系统事件的交互。 libuv 还提供了一个相似 POSIX 多线程的线程抽象,可被用于强化更复杂的须要超越标准事件循环的异步插件。 建议插件开发者多思考如何经过在 libuv 的非阻塞系统操做、工做线程、或自定义的 libuv 线程中下降工做负载来避免在 I/O 或其余时间密集型任务中阻塞事件循环。
内置的 Node.js 库:Node.js 自身开放了一些插件可使用的 C++ API。 其中最重要的是node::ObjectWrap
类。
Node.js 包含一些其余的静态连接库:如 OpenSSL,这些库位于 Node.js 源代码中的deps/
目录。 只有 V8 和 OpenSSL 符号是被 Node.js 开放的,而且经过插件被用于不一样的场景。 更多信息可查看连接到 Node.js 自身的依赖。
总结来讲,Node.js 能够理解为 js 和 c++ dll 相互工做的桥梁,而 Node.js 自身也提供了扩展 c++ dll 调用的插件机制。
同理,Electron 也能够从这个机制中获益。
目前,主流的 .dll 调用方案的关系图以下所示:
解释以下:
node-ffi 本质上是 Addons 机制下,进行过抽象封装的方案
addons插件须要针对对应版本的Node.js编译后,才能被对应版本的Node.js进行调用;换言之,若是addons插件在编译时的目标版本是 Node.js v8.3.1,那么它编译后的代码就不能被 Node.js v6.0.0的版本进行调用
node-gyp 能够帮助开发者脱离当前全局安装的Node.js版本,指定任意 Node.js 版本进行模块的编译,在编译前,须要下载对应版本的原生模块头文件,头文件的默认下载地址为 nodejs.org
Electron想要调用 .dll 文件,也须要进行对 addons 插件的编译,编译用的头文件也须要额外下载,和Node.js不一样,Electron对应的头文件的默认下载地址为 atom.io/downloaded
头文件的版本必须与 调用者 ( Node.js 或者 Electron )版本一致,这样 addons插件(包括 自定义 addon 和 node-ffi)才能正确运行
Node.js的原生模块编译,经过 node-gyp 能够比较方便地进行编译
Electron的原生模块编译,因为头文件与 Node.js 的头文件并不一致,直接用 node-gyp 进行编译的话,还须要进行一些额外的配置(头文件下载地址、版本映射等),相对没这么方便。幸亏,开源社区已经准备好了一个封装好的工具 Electron-rebuild ,它底层原理也是使用 node-gyp 进行编译,不过就不须要开发者进行额外的配置了
根据 Electron 版本的不一样(主要是 v4 和 之前的版本不一样),须要在应用中执行额外的代码
编译环境要提早准备,三大操做系统(Windows、MacOS、Linux)各不相同,看官须要根据 node-gyp 的文档,提早调整好本身的编译环境。参考文档(截至 2018-12-17):node-gyp.readme
万事俱备,咱们把源码准备好,按照 node-gyp的教程准备好编译环境,开始操做:
本次的案例方案为 Node-FFI,想要自定义addon的看管,能够先了解Electron addon的编写后,再进行编译 和 使用
在项目路径下,
安装好全部依赖
npm install
复制代码
安装 node-ffi、ref、ref-array:
npm install node-ffi --save
npm install ref --save
npm install ref-array --save
复制代码
全局安装好 Electron-rebuild
npm install -g Electron-rebuild
复制代码
假设咱们的Electron版本为 v3.0.11,32位应用
在项目路径下,执行 Electron-rebuild 命令,从新编译 node-ffi、ref、ref-array 原生模块:
Electron-rebuild -v 3.0.11 -a ia32
复制代码
如无心外,编译成功后,咱们就能够经过 Electron 应用调用 ffi 和 ref 模块了
var ffi = require('ffi');
var ref = require('ref');
复制代码
使用 ref 定义好数据类型,由于 c++ 的数据类型的内存模型不可能和 js 的是一致的,使用时,须要利用 ref 库进行转换
var intPointer = ref.refType('int');
var doublePointer = ref.refType('double');
var charPointer = ref.refType('char');
var stringPointer = ref.refType(ref.types.CString);
var boolPointer = ref.refType('bool');
复制代码
pointer,能够理解为对应 c++ 里面的指针,pointer.ref() 则是获取指针对应的数据
使用 ffi 链接 .dll 文件
var usbLib = ffi.Library(libpath, {
'InitSDK': ['int', ['pointer']],
'GetData': ['int', ['char', stringPointer]],
'ClearData': ['int', [charPointer, charPointer]],
'GetRemovableDrives': ['int', [stringPointer]]
});
复制代码
libpath 为 .dll所在的路径,相对路径与绝对路径都可,考虑到后续的安装包打包,建议为相对路径
把链接好的dll使用起来
var data = new Buffer(1000);
var result = usbLib.GetData(driveName, data);
var resultStr = '';
if(result === 0){
resultStr = wideCharBufferToString(data);
}
return resultStr;
复制代码
如上述代码,js 调用 .dll中定义好的 GetData 方法
.dll中的C++源码以下:
int GetData(string driveName, char *data) 复制代码
函数调用结果,经过 data 参数返回,调用状态 经过 int 的数据格式返回到 js 的 result 变量中
js中的data,是一个 ref 生成的 StringPointer(其实是经过Buffer扩展出的数据结构)
当函数调用结束,函数的结果也以指针的形式赋值给了data
接下来,把data这个指针指向的数据解析出来,便可获取函数的返回数据
截至 2018-12-17
一、NodeJs v10.x 与 Electron v3.x 对应的原生模块头文件,都没法和 Node-FFI latest 版本完成编译(互相不兼容)
解决方法:
开源社区上已经有开发者提交了 Node-FFI 的 PR 并经过了测试
开发人员能够先安装 PR 版本的 Node-FFI ,实测能够正常编译与正常使用
npm install node-ffi/node-ffi#169773d
复制代码