探究Rust和Node.js以及浏览器环境的交互

Mar 19

省流

如果你懒得看这篇博客,我准备了两个模板项目,直接克隆到本地就可以使用:

  • 使用Rust开发Node.js模块:neon-starter, 基于Neon框架,用于编写一些小型模块,如果你需要编写大型项目,更推荐napi-rs
  • 使用Rust开发浏览器可用的方法,基于WebAssembly技术:rust-wasm-starter

推荐使用node的工具degit进行克隆,这样不会克隆.git目录,方便自己初始化新的git项目。

npx degit <仓库地>

引言

不知道为啥,互联网上掀起了一股R RewriteI ItI InR Rust的风潮。顾名思义就是把一些比较依赖运行效率的软件通过Rust重写,由于Rust的静态编译特性,使得代码运行速度会快于大多数动态语言 (虽然编译速度很慢),所以我也在不久前(23年底大概)开始学习Rust语言。由于平时本来就和Node.js打交道比较多,再加上Evan You(尤雨溪)团队开始使用Rust开发新的前端构建工具rolldown,所以就想研究下能不能用Rust来编写Node.js和在浏览器中可用的函数。 (其实就是想多写写Rust熟悉一下)

与Node.js的交互

我主要研究了两个框架:Napi-rsNeon,他们都是用来开发Node.js模块的框架,但是在使用体验上有一些区别。

Napi-rs和Neon的区别

Napi-rs比Neon设计更优秀

编码

其实从编码角度上来说,Napi-rs是比Neon设计更优秀的,因为它对函数的包装只需要一个#[napi]宏,就可以在编译时将函数转换为JavaScript可用函数:

use napi_derive::napi;
 
#[napi]
fn fibonacci(n: u32) -> u32 {
  match n {
    1 | 2 => 1,
    _ => fibonacci(n - 1) + fibonacci(n - 2),
  }
}

如果你使用Neon,需要自己处理数据类型的转换,会写出不少冗余代码(虽然有neon-serde这种简化操作过程的库,不过还是比较麻烦)。

例如,你从函数获得的参数需要手动转为Rust可读的类型,然后返回时要转换为JavaScript可读的类型。

use neon::{
    prelude::{Context, FunctionContext}, result::JsResult, types::JsString
};

// Echo your text in param
pub fn echo(mut cx: FunctionContext) -> JsResult<JsString> {
    // read the first param of function
    let get_str: String = cx.argument::<JsString>(0)?.value(&mut cx);
    let result: String = format!("Echoed text: {}", &get_str); 
    Ok(cx.string(result))
}

文档

Napi-rs的文档较为详细, 而Neon的文档甚至感觉还不完善。

TypeScript支持

Napi-rs会自动生成TS类型声明文件*.d.ts,为TypeScript提供更好的支持,而Neon默认没有提供这样的功能。

Neon比Napi-rs的工作空间更轻量化

通过脚手架初始化两个框架的项目,你会发现,Napi-rs的工作空间里面文件很多,而Neon则比较轻量化,可以对比Napi-rs官方文档以及Neon官方文档中两者项目结构的区别。如果你只想开发一个轻量级库,推荐使用Neon,大型项目或者较为正式的项目推荐Napi-rs

提示

如果您更喜欢pnpm, npm init 命令可以用 pnpm create 命令代替。

  • Napi-rs项目的初始化:
    npm i -g @napi-rs/cli
    napi new
  • Neon项目的初始化:
    npm init neon <你的项目名>
    # 或者pnpm
    pnpm create neon <你的项目名>

两者最主要的区别基本就这些,当然还有一些细小的区别可能我还没有探究出来。总之根据自己的实际情况,选择适合自己的框架即可。

与浏览器(Web项目)的交互 —— WebAssembly

对于 Web 平台而言,WebAssembly 具有巨大的意义——它提供了一条使得以各种语言编写的代码都可以接近原生的速度在 Web 中运行的途径,使得以前无法在 Web 上运行的客户端应用程序得以在 Web 上运行。(摘自MDN)

通过WebAssembly(下称WASM),我们可以将Rust编写的方法暴露给浏览器环境中的JavaScript使用。Rust提供了wasm-pack工具用来将Rust Library构建成为WebAssembly模块,同时配合wasm-bindgencrate来对Rust函数进行宏标注,这样就能指定哪些函数会被编译到WASM中。

如何将Rust函数编译成WebAssembly模块

重要

并不是所有函数都支持被打包到WASM,比如一些引入了第三方库的方法,举个🌰:使用mysql crate操作数据库的函数。(别问我怎么知道的)

你可以在Rust项目中专门创建一个wasm crate,然后从其它crate中引入需要编译进WASM中的函数:

use demo::add_two;
use wasm_bindgen::prelude::wasm_bindgen;

#[wasm_bindgen]
pub fn add(left: u8, right: u8) -> u8 {
    add_two(left, right)
}

编译WASM时,你只需要对wasm这个crate单独运行编译命令即可。

注意

要在浏览器环境中使用,请加上--target web--out-dir可以指定输出目录,默认为pkg,生成目录的相对路径为wasm crate的路径。

wasm-pack build ./wasm --target web --out-dir output

这样就能编译出对应的WASM模块了。

如何在前端项目中调用WebAssembly模块

小贴士

如果想体验WebAssembly模块在前端运行,可以来我的这篇文档

通过wasm-pack打包的WASM模块中,包含了wasm_bg.wasm文件和wasm.js,他们两个均可以作为WASM的入口,但是使用方式有所区别:

使用wasm_bg.wasm文件作为入口

import init from './wasm_bg.wasm?init'

init().then((instance) => {
  // 假设你暴露了一个test函数
  instance.exports.test()
})

.wasm文件没有暴露出函数,所以我们只能在wasm模块初始化完成后拿到实例才能取得内部暴露的函数,这种方式很难将函数应用到JavaScript代码中。

使用wasm.js作为入口

import init, { add } from "xxx/output/wasm.js"

function addTwo() {
    const content = document.getElementById("content")
    const left = document.getElementById("left").value
    const right = document.getElementById("right").value
    const result = add(left, right)
    content.innerText = result
}

init().then()

在JS入口中,已经提前暴露出了内部函数,你可以将函数运用在任意其他JavaScript代码段中,但是,函数的执行时间必须在init().then()初始化函数执行之后

在Vite中使用WebAssembly

如果要在Vite中使用WASM,请安装vite-plugin-wasm, 并且在vite.config.ts进行如下配置:

重要

对于旧版浏览器不支持顶层await的,需要安装vite-plugin-top-level-await

// vite.config.ts
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";

export default defineConfig({
  plugins: [
    wasm(),
    topLevelAwait()
  ]
});

>
CC BY-NC-SA 4.0 2023-PRESENT © Vincent-the-gamer | Version: v1.1.1