你可以通过将密集型任务卸载到Rust中来提高你的Electron应用程序的性能。
现在有两个主要的库可以帮助你做到这一点。Neon和napi-rs。就目前而言,Neon更受欢迎,在Github上有5700颗星,而napi-rs只有800多颗。
话虽如此,但星星并不代表一切对于我的用例(截至本文写作时),napi-rs支持一个Neon还没有的重要功能:Rust能够多次回调JS回调函数。
我去寻找一个最小的启动项目来使用Electron + napi-rs,但没有找到任何东西。因此发了这个帖子 :)
TL;DR:如果你只是想克隆这个项目,你可以在Github上找到electron-napi-rs。
这篇文章的其余部分解释了这些部件是如何组合在一起的。
(btw 如果你想用Neon代替napi-rs,可以看看Mike Barber的electron-neon-rust,它基本上是我在这里做的事情的Neon版本)
一个使用Electron和napi-rs的极简项目
我从electron-quick-start的官方Electron启动器开始。这将使一个Electron应用出现在屏幕上。
然后我加入了Rust模块。这或多或少是从napi-rs的napi-derive-example中复制粘贴的,只是改变了一些相对路径。
我把Rust模块放在Electron项目中一个叫做hi-rust 的目录下。我们只需要添加4个文件。
Cargo.toml
[package]
authors = ["LongYinan <lynweklm@gmail.com>"]
edition = "2018"
name = "hi-rust"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
napi = "1.7.5"
napi-derive = "1.1.0"
[build-dependencies]
napi-build = "1.1.0"
(修改为使用版本号而不是[dependencies] 和[build-dependencies] 的相对路径 )
build.rs
extern crate napi_build;
fn main() {
use napi_build::setup;
setup();
}
(直接从napi-derive-example中取出)
这个build.rs 文件对Rust来说是特殊的。你可以在《Cargo》一书的Build Scripts部分阅读更多内容,但基本上Rust会寻找一个build.rs 文件,并在构建前运行它,如果它存在的话。
src/lib.rs
然后是代码本身,在src 文件夹下:
#[macro_use]
extern crate napi_derive;
use napi::{CallContext, Error, JsNumber, JsObject, JsUnknown, Result, Status};
use std::convert::TryInto;
#[module_exports]
fn init(mut exports: JsObject) -> Result<()> {
exports.create_named_method("testThrow", test_throw)?;
exports.create_named_method("fibonacci", fibonacci)?;
Ok(())
}
#[js_function]
fn test_throw(_ctx: CallContext) -> Result<JsUnknown> {
Err(Error::from_status(Status::GenericFailure))
}
#[js_function(1)]
fn fibonacci(ctx: CallContext) -> Result<JsNumber> {
let n = ctx.get::<JsNumber>(0)?.try_into()?;
ctx.env.create_int64(fibonacci_native(n))
}
#[inline]
fn fibonacci_native(n: i64) -> i64 {
match n {
1 | 2 => 1,
_ => fibonacci_native(n - 1) + fibonacci_native(n - 2),
}
}
(也是直接从napi-rs repo出来的)
它向JavaScript暴露了两个Rust函数:test_throw 和fibonacci 分别被暴露为testThrow 和fibonacci 。
init 实际上是JS <-> Rust绑定的 "入口",这个文件可以调用你想要的任何Rust代码。
package.json
运行npm init -y ,初始化一个默认的package.json,然后添加 "build "和 "install "脚本。
构建脚本依赖一个软件包来复制出已构建的Rust二进制文件,所以用npm install -D cargo-cp-artifact 。
{
"name": "hi-rust",
"version": "1.0.0",
"description": "",
"main": "index.node",
"scripts": {
"install": "npm run build",
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"cargo-cp-artifact": "^0.1.4"
}
}
build 脚本有效地做了两件事:
cargo build编译Rust模块,并将编译后的文件保存在数据库中。target/debugcargo-cp-artifact将输出的文件复制到项目的根目录下,作为index.node
install 脚本只是让它更容易运行。(你可以用npm i 来代替npm run build)
发布构建
Cargo默认会编译一个Debug版本,这个版本比较慢,比较大,但是包含了调试符号。
如果你想让它更快更小,请确保编译一个Release版本。如果/当你想这样做的时候,在cargo build 命令的末尾加上--release 标志。
我马上就这么做了,因为我的应用程序在Debug模式下要慢得多。
旁白:index.js与index.node?
在我设置这个的时候,发生了一件有趣的事情!
起初我根本没有改变 "main",而是将其值保留为默认的index.js 。这......工作得非常好,尽管只有一个index.node文件(没有index.js)。
我猜Node知道如果它找不到index.js ,就会去找index.node ?
总之,这有点让人不安,所以我把 "main "键改为直接指向index.node ,这样也能正常工作。我想最好是把它指向一个实际存在的文件🤷,至少可以在导入过程中减少几个周期,嗯?
建立index.node
在hi-rust 目录下运行npm install 将会下载所需的包,并构建index.node 文件,这是我们的原生 Rust 代码,经过打包后,Node 可以require() 。
添加Rust模块为依赖项
回到顶层的Electron项目中,将Rust模块作为一个依赖项添加到package.json中。
{
...
"dependencies": {
"hi-rust": "./hi-rust"
}
}
然后运行npm install ,它将建立一个与项目的链接。
从这里开始,你可以修改和重建Rust项目(在hi-rust 内),而不需要重新运行npm install 。
用preload.js暴露Rust模块
我们有了本地代码,它被打包并构建为一个模块,Node可以导入。现在我们需要在Electron应用程序中导入它。
有两种方法可以做到这一点:一种是不安全的方法,另一种是更好的方法。
不安全的方法是设置nodeIntegration: true ,这样我们就可以直接从Electron的渲染器进程中require() 节点模块。这使得代码更加简单,但主要的缺点是它打开了一个巨大的安全漏洞。
为什么不在Electron中设置nodeIntegration: true
在不安全的设置下,由渲染器运行的任何JS都可以完全访问用户的系统。这意味着文件API、网络API、进程API,等等,等等。
它可以做任何用户可以做的事情。比如下载并运行一些恶意程序,或者勒索他们的主目录。
用nodeIntegration: true 编写代码,可以稍微减少麻烦,但代价是一个巨大的安全漏洞。
更好的方法
更好的方法是使用Electron的preload 文件来有选择地将功能暴露给渲染器进程,也就是 "主世界",这就是我们要做的。
在main.js ,Electron的启动项目设置了preload.js 作为指定的预加载文件。预加载器可以访问Node的API和浏览器的API,但最关键的区别是它是孤立的:渲染器不能从预加载中调用东西,除非预加载明确地暴露了它。
因此,我们从preload.js 公开了我们的 Rust 模块,就像这样。
// Import the Rust library and expose it globally as `rustLib`
// in the renderer (also accessible as `window.rustLib`)
const rustLib = require('hi-rust')
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('rustLib', rustLib)
请注意,这是对整个库的暴露你会想暂停并思考一下,从安全的角度来看,这是否是一个好主意。如果恶意代码可以调用你的库的任何函数,会发生什么?
作为一个潜在的更安全的选择,你可以暴露单个函数...
contextBridge.exposeInMainWorld('rustLib', {
fibonacci: rustLib.fibonacci
})
或者将调用包在一个函数中,以确保只允许某些参数通过,或者做其他检查。
contextBridge.exposeInMainWorld('rustLib', {
fibonacci: (num) => {
if (num > 42) return;
return rustLib.fibonacci(num);
}
})
你也可以使用Electron的IPC系统,在主进程和渲染器进程之间来回发送请求。
在renderer.js中从Electron调用Rust代码
现在我们终于可以从渲染器中调用Rust函数了
一旦DOM准备好了,我们就调用rustLib.fibonacci ,引用来自预加载脚本的暴露的全局rustLib ,并将结果存储在一个元素中(我们仍然需要创建)。
window.addEventListener('DOMContentLoaded', () => {
const result = rustLib.fibonacci(8);
const content = document.querySelector('#rust-content');
content.innerHTML = `This number came from Rust! <strong>${result}</strong>`;
});
如果你现在运行这个,你可能会得到一个错误,比如 "无法访问null的属性innerHTML",因为这个元素还不存在。
让我们添加一个带有id="rust-content" 的 div 来包含结果。
index.html
<html>
<!-- snip -->
<body>
<!-- snip -->
<div id="rust-content"></div>
</body>
</html>
这就成功了!
这时你应该可以从顶层(Electron)目录中运行npm start ,应用程序应该弹出一个由Rust计算的数字:)
...同步进行!
有一点需要注意,这是对Rust的同步调用。如果fibonacci函数超级慢,或者我们要调用其他一些阻塞的函数,我们的应用程序就会冻结了。
你可以自己试试:试着给fibonacci传递一个大数,比如1234 ,而不是8 。
帮助!错误!
下面是我在使用过程中遇到的几个错误,以及我是如何解决的。如果你一直在学习,你可能不会遇到这些错误,但我在这里列出它们,以防万一。
缺少package.json
当我忘记在Rust库的目录中创建一个package.json ,我就遇到了这个错误。
Internal Error: Cannot find module '/Users/dceddia/Projects/electron-napi-rs/hi-rust/package.json'
Require stack:
- /usr/local/lib/node_modules/@napi-rs/cli/scripts/index.js
Require stack:
- /usr/local/lib/node_modules/@napi-rs/cli/scripts/index.js
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
at Function.Module._load (node:internal/modules/cjs/loader:778:27)
at Module.require (node:internal/modules/cjs/loader:1005:19)
at require (node:internal/modules/cjs/helpers:94:18)
at getNapiConfig (/usr/local/lib/node_modules/@napi-rs/cli/scripts/index.js:23450:19)
at BuildCommand.<lt;anonymous> (/usr/local/lib/node_modules/@napi-rs/cli/scripts/index.js:23579:30)
at Generator.next (<lt;anonymous>)
at /usr/local/lib/node_modules/@napi-rs/cli/scripts/index.js:65:61
at new Promise (<lt;anonymous>)
at __async (/usr/local/lib/node_modules/@napi-rs/cli/scripts/index.js:49:10)
最后的解决方法很简单:npm init -y 创建了一个package.json 文件并解决了这个错误。
从Electron的导出不正确preload.js
我第一次尝试将Rust库暴露在Electron的渲染器进程中是这样的:
const rustLib = require('hi-rust');
window.rustLib = rustLib;
我能够很好地启动Electron,但是它在浏览器控制台中记录了一个错误,表明window.rustLib 是未定义的......这意味着我这一行被忽略了:
Uncaught TypeError: Cannot read property 'fibonacci' of undefined
我想这是因为contextIsolation 默认是打开的,所以任何添加到window 对象的东西都不会被看到。
修复方法是使用Electron的contextBridge 模块,特别是exposeInMainWorld 函数。
preload.js
const rustLib = require('hi-rust');
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('rustLib', rustLib)