使用Electron/Web开发的踩坑记录
之前在Python的GUI编写上有过一些简单的尝试,后来想到结合FastAPI的后端,自己再写一个前端熟悉一下HTML5/CSS/JS三件套,又听说Electron是Web套皮做GUI的不错的选择,所以也做了一些尝试,但是遇到了不少的坑。老实说,有些地方的文档实在是写了和没写差不多……
下面在假定已经完成了Electron的安装,写好了Web页面的基础上,介绍一些经验。
基于Napi实现C++和Node.js交互
最初写好的Web的核心逻辑完全依赖Web上的API,而这一部分我已经写过C++的实现,所以自然地想到在Node.js里调用C++,然后向Web页面提供这部分功能。
最开始搜到的实现方法是node-ffi
,这个模块提供了和C++编写的动态链接库的交互,具体用法和Python的ctypes大同小异,也是注册接口,定义输入参数和返回值的数据类型,做好转换就完成了。遗憾的是,这个模块在2018年就已经停止维护,貌似并不支持新版的Node,不能通过编译,而我也不愿意专门为此倒退版本,于是作罢。
在这之后,就只有在C++中编写原生接口的办法了。当然这里也有两种选择,一种是直接基于v8,一种是相对简单一些的Napi,我没有时间精力慢慢研究v8的用法,就用了相对简单的Napi。就是在这一步,我被官方文档折磨了很久,还是通过网上各种代码片段才悟出了逻辑。当然也可能是因为我太菜,毕竟确实没有系统学习过C++,都是现查现用。
下面贴上一小段代码,作为最小可用的简单实现。
1 |
|
首先是引入Napi的头文件,这个没什么可说的。
接下来是Encrypt函数,这里包含了我的核心逻辑,也是要供nodejs调用的方法。这里接受的传入参数info
既包含了调用时的上下文,可以通过info.Env()
获取,也可以通过索引取出nodejs调用时具体传入的参数。
传入参数在Napi中的数据类型都是Napi::Value
,这是Napi里大部分数据的基类。具体使用时,就涉及类型转换的问题,所有继承自Napi::Value
的类都有一个.As<T>()
的数据转换方法,可以转换成和JS对应的具体数据类型。在这里,我编写的接口接受的是一段字符串编码成的Buffer,所以转换成Napi::Buffer<char>
。这里Napi::Buffer
是一个模板类,而我希望按char
的方式取值。
对于这一类数据,可以通过.ByteLength()
获取大小,也可以通过.Data()
获取包含了数据的指针。其他使用较多的Napi::String
也有一些比较简单的用法,比如转成UTF-8编码的char字符串,这里就不作赘述了。
输出时,自然也要返回特定的nodejs数据类型。在上面的实例代码中,我使用了New(env, size_t length)
的方式构建,当然这个方法有很多其他的重载,都可以在文档中一一查到。注意:虽然New
确实可以直接接受包含数据的指针,但意外的是,构建之后数据似乎并没有复制到新建的类里面,所以在这里我又不得不手动复制了一遍。
接下来是相对容易解决,不需要改动的地方。Init
函数里规定了要导出的方法及其命名,最后再通过NODE_API_MODULE(name, Init)
完成导出。
到这里发现,其实这个过程还是比较顺畅的,我主要是在查找用法的时候遇到了困难。
模块的编译
这里是一个大坑,也是我最痛苦的地方。用C++编写完接口之后,就要用node-gyp
编译成nodejs中可调用的二进制文件。一般把node-gyp
安装为全局模块:
1 | npm install -g node-gyp |
在Windows上,node-gyp
使用时依赖Visual Studio Build Tools
和Python
,分别用作编译的部分和生成项目文件的部分。注意是否添加了环境变量。
首先写好编译的配置文件bindings.gyp
:
1 | { |
大部分键值对都是字面意思,别的地方也很容易找到,这里主要讲我自己遇到的问题。我的源码用到了openssl库里的加密算法,而MSBuild使用的编译器默认包含的头文件里并没有openssl,所以我只好在include_dirs里加入了自己安装了MinGW
里的头文件目录。另外,openssl使用的时候还会依赖对应的链接库,这也需要手动添加对应的路径。我尝试过添加dll路径,但是编译时无法识别,最后只能添加了Strawberry Perl里的静态链接库libcrypto.a
和libssl.a
,见示例配置文件的conditions
字段。实际上node-gyp
编译的时候本身貌似可以调用环境变量中的动态链接库,但是后续重新编译的时候就不行了,这是最困扰我的部分。
完成后,使用node-gyp
生成VS的项目文件并编译:
1 | node-gyp configure rebuild |
值得注意的是,这个MSBuild对编码的支持非常申必,它识别不了不带BOM的UTF-8源码,但是编译之后,里面用到的汉字常量又变成了GBK编码,令我困惑无比。
编译完成后,在纯nodejs中很容易引入:
1 | const test = require('bindings')('test') |
Electron再编译
然后问题又来了,虽然在纯nodejs中可以正常运行,但是上面的代码拿到Electron里面运行又会报错。在网上查到,需要用到Electron配套的工具重新编译一遍才能供Electron正常使用。这里用到的模块是electron-rebuild
,安装方法类似,使用时要执行目录下的二进制文件:
1 | ./node_modules/.bin/electron-rebuild.cmd |
可以看到,这里不需要额外的参数。上面提到的链接库的问题其实也主要是在这一步遇到的。
完成编译之后,就可以在Electron内正常调用了。
Electron跨进程通信极简用法
最后再简单讲讲Electron主进程和渲染进程的交互。在最开始的地方我已经提到,我所做的的工作是要把处理信息的核心逻辑移到nodejs中,通过调用C++模块的方式实现,这里就涉及到了ipc
的一些基本用法。
首先,在Web页面的js文件中引入ipc
模块:
1 | const ipc = require('electron').ipcRenderer |
发送信息也非常简单:
1 | //send(name, data) |
在主进程中要引入另一个对应的模块,使用时监听对应信道:
1 | const ipc = require('electron').ipcMain |
注意在向渲染进程传回信息时,可以通过event.sender
原路返回对应的进程。