Svelte是一个JavaScript框架,在Web应用开发者中迅速流行起来。但是,如果我们想使用Rust编程语言来代替JavaScript或TypeScript的速度、安全和其他好处呢?我们可以这样做,这要感谢WebAssembly。
WebAssembly像TypeScript一样是类型安全的,而且由于它是事先编译的,而不是在运行时编译的,所以甚至比JavaScript还要快。结合Svelte的易用性和它在没有虚拟DOM的情况下更新用户界面的能力,我们甚至可以使重型应用程序快得惊人
在这篇文章中,我们将看到如何通过将Rust编译成WebAssembly来连接Svelte前端和Rust代码,以及如何在JavaScript和Rust之间调用函数和传递数据。具体来说,我们将涵盖。
- 什么是Svelte?
- 什么是Wasm?
- 为什么要在Rust中使用Wasm?
- 用Rust、Wasm、Node和其他软件包设置我们的系统
- 设置我们的Svelte+Wasm+Rust项目
- 连接Svelte和Rust
- 添加风格与暴露结构和方法
- 在JavaScript和Rust之间传递复杂的数据类型
- 增加Wasm的内存大小
你需要具备Rust的基本知识才能跟上本教程的进度。这可能包括读、写和理解变量声明、if…else 块、循环和结构。对Svelte有一点了解也会有帮助。
什么是Svelte?
Svelte是另一个前端框架,类似于React或Vue。和这两者一样,你可以用Svelte来制作单页应用程序。然而,Svelte有一些特点,使其与其他框架有显著的不同。
例如,Svelte与其他框架不同,主要是一个编译框架;Svelte库的大部分是devDependency ,而不是运行时依赖。这有助于使最终的应用程序更小,客户下载速度更快。
另一个区别是,Svelte不使用虚拟DOM。相反,它使用各种策略,只更新页面中被改变的特定部分。这减少了开销,使应用程序的速度更快。
Svelte还通过不需要类或函数来声明组件来提供易用性。每个Svelte文件本身都被当作一个组件来处理。
最后,Svelte的钩子非常少。尽管生命周期功能需要钩子,但Svelte不需要复杂的状态管理钩子。状态可以简单地通过声明一个变量来存储和使用,就像在vanilla JavaScript中一样。
什么是Wasm?
WebAssembly(Wasm)是一种二进制指令格式,我们的浏览器可以和JavaScript一起运行。
然而,与JavaScript不同的是,它已经是二进制格式,在编译时已经解决了类型信息,并且不需要解释或即时编译。由于这些和其他一些原因,在许多情况下,它可以比JavaScript快很多。
Wasm是一个编译目标,几种语言--包括Rust--都可以编译到它。因此,我们可以用Rust编写我们的程序,将其编译为Wasm,并在浏览器中使用JavaScript运行它。
为什么使用Wasm和Rust?
在Rust中使用Wasm有很多好处。让我们看一下几个实际的项目例子。
你可以用Rust编写一个包含核心功能的库,然后通过编译到Wasm,将其用于网络应用和桌面或命令行应用。一个这样的例子是8086模拟器。
在8086模拟器中,核心模拟器和编译器被写在一个与平台无关的库中,然后通过编写一个薄的平台特定的接口来适应命令行和网络格式。
另一个伟大的用途可以是快速获得库的交互式演示,而不需要编写复杂的命令行程序或纯文本的例子。我们可以创建一个图形化的网络界面,并从事件处理程序(如按钮点击)中调用库的功能。
看一下这个网络界面的 [pcb-rs](https://yjdoc2.github.io/pcb-rs-examples/) 库的例子,它展示了这个用例。这个例子提供了一个硬件模拟库的演示,同时还提供了一个用Svelte编写的互动界面来玩。
这不仅限于小项目;甚至巨大的项目,如Figma,也使用Wasm将他们的桌面应用程序改编为网络,以便他们可以方便和容易地使用。
作为一个附带说明,Wasm不是服务器端逻辑的替代品。它仍然运行在客户端的浏览器中,所以你仍然需要一些东西来服务Wasm文件,即使它只是一个静态文件服务器。
用Rust、Wasm、Node和其他软件包设置我们的系统
让我们从设置系统开始。
首先,按照其官方网站上的说明安装Rust。这将在你的系统上安装和设置Rust语言工具,如编译器和标准库。
然后,安装 [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/),这是一个用于将Rust编译成Wasm的辅助工具。这个工具还负责下载编译到Wasm所需的工具链。
最后,按照安装指南安装Node.js、npm和npx。你可能需要将npx作为一个全局包单独安装。
为了检查一切设置是否正确,尝试运行以下命令,看看是否有什么地方出现错误。
> npm --version
> npx --version
> node --version
> cargo --version
> rustc --version
> wasm-pack --version
> rustc --print target-list # This list should have wasm32 targets
如果这些命令运行时没有任何错误,那么我们就成功完成了系统设置。
设置我们的Svelte+Wasm+Rust项目
有多种方法来设置一个使用Svelte、Wasm和Rust的网络项目。比如说 [wasm-pack](https://github.com/rustwasm/wasm-pack) 来自 [rust-wasm](https://github.com/rustwasm/wasm-pack) 组提供了一个简单的HTML-CSS-JS应用的模板。也有几个项目模板用于React或Svelte与Rust和Wasm。
我们将使用一个npm插件来代替任何模板,这将为我们的项目结构提供更大的灵活性。
创建我们的Svelte应用程序
首先,创建一个项目目录。在这个目录中,我们将首先通过运行下面的代码,使用他们的模板创建一个Svelte应用程序。
> npx degit sveltejs/template svelte
在上面的代码中,我们将我们的应用程序命名为svelte ,但你可以根据自己的需要来命名它。
同样在项目目录下,运行下面的程序来创建一个Rust库项目目录。
> cargo new --lib rust
在上面的代码中,我们将我们的库命名为rust ,但你可以给它一个不同的名字。
现在,进入svelte 目录,并运行下面的命令来安装基本的依赖项。
> npm install
之后,我们将安装rollup-plugin-rust ,它将自动将Rust代码编译成Wasm,并允许我们轻松地将Rust中的东西导入到JavaScript中。
> npm install @wasm-tool/rollup-plugin-rust
接下来,打开rollup.config.js 文件,添加以下导入语句。
import rust from '@wasm-tool/rollup-plugin-rust';
在同一个文件中,在serve 函数之后,会有一个export default 语句。这条语句导出了配置对象。在该配置对象中,将有一个plugins 数组。我们需要将Rust插件添加到这个数组中,像这样。
...
plugins: [
rust({
verbose: true,
serverPath: "build/"
}),
svelte({
...
该插件提供了几个选项。最重要的是,对于我们的项目来说。
verbose将显示编译步骤和它的输出,只要它编译你的Rust代码serverPath指定Wasm文件将从哪个路径提供
我们将serverPath 设置为build ,因为在开发模式下,Svelte从build 目录中提供文件。如果我们想部署我们的项目,serverPath 需要做相应的调整。
再次运行npm install 来安装这个插件。至此,我们的Svelte应用程序的设置应该结束了。
为我们的项目设置Rust
为了给我们的项目设置Rust,将目录改为rust 。打开Cargo.toml 文件,在[package] 键之后和[dependencies] 键之前添加以下内容。
[lib]
crate-type = ["cdylib", "rlib"]
在[dependencies] 关键中,添加以下内容。
wasm-bindgen = "0.2.63"
运行一次cargo build ,以获取并安装依赖项。
这样,项目的基本设置就准备好了。
连接Svelte和Rust
我们将首先连接Svelte和Rust,从Rust中公开一个简单的add 函数,并从Svelte中调用它。这一步很简单,但会帮助我们验证两者是否连接并按预期运行。
在rust 目录中,打开src/lib.rs 文件并删除其中的默认测试。在现在的空文件中,添加以下内容。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: usize, b: usize) -> usize {
a + b
}
在上面的代码中,我们首先从wasm_bindgen 中导入了prelude ,它提供了编译和绑定Rust代码到Wasm所需的宏和其他项目。
另外,我们想暴露给JavaScript的任何东西都需要是公开的,因此在函数上有pub 这个关键字。同时,#[wasm_bindgen] 提供必要的绑定 "胶水"。
现在,回到svelte 目录中,打开main.js 文件,添加以下import 命令。
import wasm from '../../rust/Cargo.toml';
在上面的代码中,我们称导入为wasm ,但你可以随心所欲地命名它。路径应该指向我们要编译的项目的Cargo.toml 文件,并将其导入为Wasm。我们可以像这样导入多个项目,用不同的名字和相应的路径。
有了这个,我们之前安装的插件将拦截导入,编译Rust代码,设置Wasm,并为我们把它连接到我们的Svelte应用程序上
因为Wasm的导入需要以异步方式进行,所以我们将把应用程序的定义和export default 语句改为如下。
import App from './App.svelte';
import wasm from '../../rust/Cargo.toml';
const init = async () => {
const bindings = await wasm();
const app = new App({
target: document.body,
props: {
bindings,
},
});
};
init();
在上面的代码中,我们定义并调用了一个异步函数,它将等待导入,然后将其作为一个道具传递给应用程序。Rust库使用#[wasm_bindgen] 暴露的所有东西现在都可以在这个导入的模块中访问;因此,我们的add 函数可以作为bindings.add 。
现在我们将删除App.svelte 文件中的所有内容,并添加以下内容。
<script>
export let bindings;
</script>
<h1>
{bindings.add(5,7)}
</h1>
接下来,在svelte 文件夹中运行npm run dev 。如果到目前为止一切正常,应该显示Rust正在编译(尽管在你第一次运行时可能需要一点时间)。
编译完成后,它将显示Svelte的信息,即服务器已连接到localhost 。当你打开localhost url时,它应该显示数字为12。
恭喜你,我们已经将Rust连接到了Svelte!
你可以通过添加和公开更多的函数并从JavaScript中调用它们来玩一玩。
目前,我们只能传递简单的参数,如usize,bool ,等等。然而,我们将在后面看到如何将结构和impl 方法暴露给JavaScript,以及如何从Rust中调用JavaScript函数。
在这之前,让我们为我们的Svelte应用添加一点风格。
添加风格carbon-components-svelte
本节将解释如何使用carbon-components-svelte ,为我们的Svelte应用程序设置和添加样式。我们也可以使用一些其他的库来进行样式设置,比如Material UI或Bootstrap,这不会有太大的区别。
我们将使用最基本的步骤快速添加carbon-components-svelte ,但你可以查看该库的文档以获得更详细的信息和例子。
首先,通过在svelte/ 目录中运行以下程序来安装carbon-components-svelte 。
> npm i -D carbon-components-svelte
然后,我们将把样式表导入到main.js 。它支持一些开箱即用的主题,如white 、gray10 、gray80 等,还有动态主题支持和自定义主题支持。在这个例子中,我们将使用gray80 主题。
在你的main.js 文件中添加以下import 命令。
import "carbon-components-svelte/css/g80.css";
在App.svelte 文件中做如下轻微改动。
<script>
...
import {Content} from 'carbon-components-svelte';
...
</script>
<Content>
<h1>
{bindings.add(5,7)}
</h1>
</Content>
如果你现在看一下这个页面,它将有一个漂亮的灰色背景,数字12和以前一样,现在增加了边距和填充。
虽然你可以用Svelte建立其他的网页组件,甚至建立你自己的组件库,但这就是我们要做的造型的范围。除了稍后使用的输入组件外,我们将把重点放在连接Rust和Svelte上。
暴露结构和impl 方法
接下来的部分会稍微高级和复杂一些。我们将在Rust中定义一个结构,并从JavaScript中使用它来向一个函数传递值。我们还将看到我们如何暴露impl 方法,并从JavaScript中调用它们。
首先,让我们在Rust项目的lib.rs 文件中添加一个名为Car 的简单结构。
#[wasm_bindgen]
pub struct Car {
pub number: usize,
pub color:usize, // color in hex code
}
#[wasm_bindgen]
pub fn color(a: Car,color:usize) -> Car {
Car {
number: a.number,
color
}
}
在上面的代码中,我们将#[wasm_bindgen] Rust库和一个color 函数一起添加到该结构中,该函数接收一个Car 类型的参数和一个usize 类型的color 。我们还明确地在number 和color 的字段Car 上添加了pub ,以确保它们被暴露给JavaScript。
在我们的svelte 目录中,让我们改变我们的App.svelte 文件,如下所示。
<script>
export let bindings;
import {Content} from 'carbon-components-svelte';
let {Car,color} = bindings; // destructure for easier access
console.log(Car,color);
</script>
<Content>
<h1>
</h1>
</Content>
在上面的代码中,我们解构了bindings ,以提取Car 和color 。我们还删除了对add 的调用。
如果我们看一下控制台,我们可以看到,Car 是一个类,color 是一个函数。然而,我们不能使用new Car() 直接将Car 实例化为一个类,因为从本质上讲,它是一个Rust结构,本身并不拥有一个constructor 。
因此,为了将其实例化,我们必须将new 方法添加到Car 类中,并将其公开。
#[wasm_bindgen]
impl Car {
pub fn new() -> Self {
Car { number: 0, color: 0 }
}
}
请注意,我们可以对结构的某个方法进行标记,比如上面代码中的new ,将其作为一个constructor 。不过,我们只在下一节简要地谈一下这个概念。
接下来,在App.svelte ,添加以下内容。
let {Car,color} = bindings; // destructure for easier access
let c = Car.new();
c.number = 5;
c.color = 775577;
console.log(c);
let c2 = color(c,557755);
console.log(c2.number,c2.color);
如果我们现在看一下控制台,我们可以看到第一个console.log ,显示如下的内容。
Object {ptr:...}
要理解为什么console.log(c) 会导致这种输出,我们必须理解Car 实例到底是什么。
Wasm在浏览器中的运行方式是在一个基于堆栈的虚拟机上。我们需要另外一篇文章来讨论WebAssembly的工作方式和原因。
对于我们的目的,我们需要了解的是,Wasm有一个线性内存,就Wasm而言,它所需要的一切都存储在该内存中。
当我们从Wasm获得一个对象时,它将其指针存储在对象的线性内存中。因此,我们之前记录的对象只在其线性内存中存储其位置的指针。
对于较小的应用程序,我们可以忽略这些细节,但随着应用程序规模的增长和传递的数据越来越复杂,我们需要考虑到这些细节。
例如,(几乎)所有穿过JavaScript-Wasm边界的东西都必须从该线性内存复制到JavaScript的内存中;因此,具有许多值的大型对象将需要时间和计算能力来进行复制。
另一个问题是,在默认情况下,Wasm有一个固定的1MB的内存分配给它。如果需要一次性传输的数据大于这个数字,我们将遇到内存限制错误。
事实上,由于我们的Wasm代码中的其他信息也将占用这1MB内存的一部分,我们可能会比预期的更早地遇到内存限制。如果需要,我们将在本文的最后一节看到如何增加这个限制。
回到我们当前的实现,我们已经看到了如何暴露和使用结构及其方法。接下来,让我们来看看如何公开相关的方法,或接受self 参数的函数。
#[wasm_bindgen]
impl Car {
...
pub fn duplicate(&self) -> Self {
Self {
number: self.number + 1,
mul: self.color,
}
}
...
}
在上面的代码中,我们把Car ,并使用duplicate() ,返回一个新的对象,其number ,比以前大一个,其color ,是一样的。
接下来,在App.svelte ,运行以下内容。
let c = Car.new();
c.number = 5;
c.color = 775577;
...
let c3 = c2.duplicate();
console.log(c3.number,c3.color);
console.log(c2,c3);
我们在这里可以看到,c3 的值是按复制函数设置的。ptr 更重要的是,我们可以看到c2 和c3 的值不同,表明它们确实都是不同的对象。
如果我们添加一个方法来获取Car ,如下所示。
...
pub fn change_number(&mut self,number:usize) {
self.number = number;
}
...
并像这样在c3 上调用它。
c3.change_number(7);
console.log(c3.number);
我们就会看到,c3 的数字已经改变。
这里有一个有用的提示是,每当你对哪些属性可以从JavaScript中访问感到困惑时,console.log 该对象并打开该对象的原型描述。
在该对象的原型描述中,你将能够看到JavaScript可以访问的函数和参数。事实上,你可以通过点击旁边的>> ,检查参数的值,这将显示它们的当前值。
因此,尽管我们在调用change_number 之前已经记录了c3 对象,但我们现在可以在控制台原型中展开其number 信息。通过这样做,我们可以看到它的新值而不是原来的值,因为change_number 函数以可变的方式获取Car 。
在JavaScript和Rust之间传递复杂的数据类型
到目前为止,我们所传递的数据类型都相当简单。甚至我们使用的结构也是由原始类型组成的。
我们也可以使用一些复杂的东西,比如Vec<_> ,将向量从Rust传给JavaScript。所有这些工作的方式是,wasm_bindgen 实现了derive 函数,它将Rust的值转换为JavaScript,反之亦然。
然而,那些没有被wasm-bindgen 预定义的数据类型不能被轻易地传递。例如,让我们在我们的Car 中添加一个Box<usize> ,如下图所示。
#[wasm_bindgen]
pub struct Car {
pub number: usize,
pub color:usize, // color in hex code
pub boxed_value: Box<usize>
}
...
pub fn new() -> Self {
Car { number: 0, color: 0, boxed_value: Box::new(5) }
}
现在,如果我们看一下我们运行npm run dev 的控制台,我们会看到一个错误,像这样。
error[E0277]: the trait bound `Box<usize>: IntoWasmAbi` is not satisfied
--> src/lib.rs:3:1
|
3 | #[wasm_bindgen]
| ^^^^^^^^^^^^^^^ the trait `IntoWasmAbi` is not implemented for `Box<usize>`
|
= help: the following implementations were found:
<Box<[JsValue]> as IntoWasmAbi>
<Box<[T]> as IntoWasmAbi>
<Box<[f32]> as IntoWasmAbi>
<Box<[f64]> as IntoWasmAbi>
and 10 others
= note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `Box<usize>: FromWasmAbi` is not satisfied
--> src/lib.rs:3:1
|
3 | #[wasm_bindgen]
| ^^^^^^^^^^^^^^^ the trait `FromWasmAbi` is not implemented for `Box<usize>`
|
= help: the following implementations were found:
<Box<[JsValue]> as FromWasmAbi>
<Box<[T]> as FromWasmAbi>
<Box<[f32]> as FromWasmAbi>
<Box<[f64]> as FromWasmAbi>
and 10 others
= note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
waiting for changes..
这个错误的基本解释是,当跨越Rust-Wasm-JavaScript边界传递数值时,需要名为FromWasmAbi 和ToWasmAbi 的特质。由于Box<usize> 没有实现这些traits,所以值不能被传递。
这背后的原因之一,特别是对于Box<_> ,是盒子是一个值的容器。盒子将值存储在堆上,而不是堆栈上,并管理该内存,直到值被丢弃,之后内存被释放。
将这些值传递给JavaScript需要回答这样的问题:谁在实际处理这些内存,以及JavaScript的垃圾收集器如何与这些信息进行交互。随之而来的是,由于JavaScript允许完全的可变性,它可以打破Box 需要的保证,并创造出不安全的内存状态。
由于诸如此类的原因,wasm_bindgen 默认不实现。另外,由于Box 和From/ToWasmAbi 特质都在我们的箱体之外,我们也不能为它实现。
考虑一下另一个类似的情况。假设我们使用的是另一个板块的东西,它没有实现某些特征,但我们需要在我们的结构中拥有这些特征,而这些特征又需要暴露给JavaScript。这可以通过两种方式解决。
首先,那些不能安全传递的对象或者不能使用serde-json 进行序列化的对象可以通过句柄来暴露。
其次,可以使用序列化的 对象 [serde-json](https://blog.logrocket.com/json-and-rust-why-serde_json-is-the-top-choice/)可以使用serde-json crate将其转换为JavaScript表示形式,并传递给对方。
我们将在接下来的章节中探讨这些解决方案。
使用句柄和从Rust中调用JavaScript函数
为了理解句柄的概念,请记住,wasm_bindgen 只暴露那些被标记为pub 的属性。任何私有的属性都很可能是没有实现某些特性的类型。
让我们从boxed_value 中删除pub ,并调整我们上面使用的代码中的color 函数。
#[wasm_bindgen]
pub struct Car {
pub number: usize,
pub color:usize, // color in hex code
boxed_value: Box<u8>,
}
...
#[wasm_bindgen]
pub fn color(a: Car,color:usize) -> Car {
Car {
number: a.number,
color,
boxed_value:Box::new(0)
}
}
我们的代码应该可以再次编译了!如果我们使用之前的原型技巧,我们会发现boxed_value 确实无法从JS中访问。如果你想亲自看看,你可以试着用控制台记录来确认。
这种使用Wasm不兼容的值的方法可以被认为是手柄。本质上,我们不能直接访问内部值,但我们可以访问包含这些值的结构。
因此,我们可以使用这些结构来处理这些值。因为这些值仍然可以从Rust中访问,所以我们可以通过Rust中的函数对它们进行操作。让我们删除对我们的Box 所做的所有改动,并添加另一个结构。
pub struct OwnerID {
id: usize,
}
#[wasm_bindgen]
pub struct Car {
pub number: usize,
pub color: usize, // color in hex code
pub owner: OwnerID,
}
...
pub fn new() -> Self {
Self {
add: 0,
mul: 0,
owner: OwnerID { id: 0 },
}
}
...
#[wasm_bindgen]
pub fn color(a: Car,color:usize) -> Car {
Car {
number: a.number,
color,
owner: OwnerID { id: 0 }
}
}
使用上面的代码,我们会得到和以前一样的错误。
error[E0277]: the trait bound `OwnerID: IntoWasmAbi` is not satisfied
--> src/lib.rs:7:1
|
7 | #[wasm_bindgen]
| ^^^^^^^^^^^^^^^ the trait `IntoWasmAbi` is not implemented for `OwnerID`
|
= note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `OwnerID: FromWasmAbi` is not satisfied
--> src/lib.rs:7:1
|
7 | #[wasm_bindgen]
| ^^^^^^^^^^^^^^^ the trait `FromWasmAbi` is not implemented for `OwnerID`
|
= note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
为了解决这个问题,像以前一样,我们可以在Car 中删除owner 中的pub ,这样代码就可以再次编译了。然而,现在我们没有办法从JavaScript中访问owner 中的值。
如前所述,Car 现在作为OwnerID 的句柄;我们可以通过Car 访问OwnerID的值。例如,为了从OwnerID 里面获取id 的值,我们可以在Car 上定义一个方法,如下所示。
#[wasm_bindgen]
impl Car {
...
pub fn get_id(&self) -> usize {
self.owner.id
}
...
在App.svelte ,运行如下。
console.log(c3.get_id());
现在我们可以看到id 的值为OwnerId 。同样地,我们可以定义方法来改变它的值。
这种句柄的另一个用途是当我们需要将不兼容的类型,如Box ,传递给一些方法。我们可以不公开该方法,而是公开另一个方法,该方法接受所需的值来构造不兼容的类型,然后从公开的方法里面调用不兼容的方法。
例如,假设我们有一个方法需要一个OwnerID 作为参数。我们可以用另一个方法来代替这个方法,这个方法接收usize 类型的id ,从中构造OwnerID ,然后用构造的OwnerID 作为参数来调用不兼容的方法。
要从Rust中连接并调用一个JavaScript函数,我们需要将其声明为extern "C" ,并以#[wasm_bindgen] ,之后我们就可以调用该函数。
需要注意的是,由于Rust没有函数重载或变量函数,我们需要在extern 块中声明我们要使用的每个参数组合。
例如,要从Rust中调用alert ,请按如下方式声明。
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello in JS from rust!");
}
然后,我们可以从JavaScript中调用它,像这样。
...
let {Car,color,greet} = bindings; // destructure for easier access
...
greet();
要用不同类型的参数调用alert ,你需要声明另一个函数,并将其映射到JavaScript中的同一个函数。例如,对于一个以usize 为参数的警报,你需要声明如下。
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_name = alert)]
fn alert_usize(a: usize);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello in JS from rust!");
alert_usize(5);
}
关于如何将JavaScript函数绑定到Rust函数,如何在模块内绑定函数,以及如何将一个结构的方法绑定为暴露的类的构造函数的更多详细信息,请查看 [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-js-imports/index.html) 参考资料。
在Svelte中通过Wasm使用Rust,使用serde.json 方法
现在让我们来看看如何使用我们前面提到的serde.json 方法,在Svelte中通过Wasm使用Rust的例子应用程序。
假设你正在制作一些网络应用,从用户的文件中生成大量的数字数据,然后用户可以以一种特殊的文件格式下载这些数据。你想让用户上传该文件,然后解析数据,这样你就不用再生成数据了,你选择通过Wasm来实现这个目的。
我们下面的例子中的数据格式保持简单,以避免分散我们学习如何将Rust连接到Svelte的主要目标的注意力。为这个项目生成假数据将是很容易的。
我们使用的格式有点类似于CSV文件,第一行是用逗号分隔的列名,其余的 "行 "包含了 [f64](https://doc.rust-lang.org/std/primitive.f64.html) 数据类型,用分号隔开。我们不会有任何缺失的值或使用其他类型的值。
我们的示例文件可以是这样的。
A,B,C,D;10.2,5,-6.3,-7.8;8.77,5,89,-2.56,3.33
解析这些数据的详细代码将不在这里解释,但你可以在本文末尾链接的资源库中查看它。对于我们的目的来说,重要的是。
- 该函数被定义在一个名为
parser - 该函数的名字是
parse,它接受一个&str作为输入。 - 它返回一个
HashMap<String,Vec<f64>>,其中的字符串键将是第一行中定义的列名。
让我们创建一个名为parser.rs 的新文件,并在其中写入我们的函数。
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn parse(input: &str) -> HashMap<String, Vec<f64>> {
...
}
任何公开的模块,其内容(用#[wasm_bindgen] 注释)默认都会导出到JavaScript。因此,让我们在我们的lib.rs 文件中添加以下内容。
pub mod parser;
如果我们现在试图运行我们的代码,我们会得到以下错误。
the trait IntoWasmAbi is not implemented for HashMap<String, Vec<f64>>
我们可以尝试创建一个句柄,然后通过该句柄访问数据,但这将是很繁琐的。因为我们知道,在JavaScript中,Map 、Vec 和String 对象存在兼容的数据结构,所以我们将使用之前提到的serde-json 方法。
修改Cargo.toml 文件,如下图所示。
serde = { version = "1.0.137", features = ["derive"] }
wasm-bindgen = { version= "0.2.63", features = ["serde-serialize"] }
接下来,修改函数如下。
pub fn parse(input: &str) -> JsValue {
let mut ret: HashMap<String, Vec<f32>> = HashMap::new();
...
JsValue::from_serde(&ret).unwrap()
}
现在它可以编译了,我们回到了正轨。
我们可以从JavaScript访问parse ,通过App.svelte ,如下图所示。
let {Car,color,greet,parse} = bindings; // destructure for easier access
console.log(parse("A,b,c;5;6.3;7.8"));
我们可以看到,它返回一个对象,键是列名,值是各自的数组。
接下来,我们将把它连接到前端的一个事件上,这样用户就可以上传一个文件并得到其内容的解析。
要做到这一点,只需使用carbon-components-svelte 中的FileUploader 组件。我们将首先在用户界面中显示结果,这样我们就可以知道文件被正确上传和访问。
<script>
...
import {Content,FileUploader} from 'carbon-components-svelte';
let files = []; // file handles will be added in this
let content = ""; // for us to see the contents
let reader = new FileReader();
// add listener to load event, which fires when file read is completed successfully
reader.addEventListener("load",() => {
content = reader.result; // set content in ui, so we can see it
},false);
// this will be used as callback to file uploader
let add_handler = (e) => {
reader.readAsText(e.detail[0]);
}
...
</script>
<Content>
<h1>
Connecting rust to Svelte Through wasm !
</h1>
<br />
<div>
<FileUploader
labelTitle="Upload file"
buttonLabel="Add file"
labelDescription="Only txt files are accepted."
accept={[".txt"]}
bind:files
status="complete"
on:add={add_handler}
/>
</div>
<br />
<h3>
File Contents Are :<br/>
{content}
</h3>
</Content>
如果我们刷新页面并上传一个带有假数据的文本文件,像这样。
A,B,C,D;
1.5,1.5,5.1,5.1;
7.5,5.7,5.5,7.7;
我们可以看到它上传了文件并且可以读取其内容。
现在,我们可以在事件监听器中调用parse 函数,而不是显示内容。我们将得到解析后的数据,然后可以按照应用程序的要求来使用。
这样,我们已经成功地将我们的Svelte应用程序与我们的Rust代码连接起来,并在它们之间传递数据
增加Wasm的内存大小
如前所述,默认情况下,Wasm代码有1MB的堆栈内存供其使用。这个内存量对许多应用程序来说是绰绰有余的,但有时,我们可能需要比这更多的内存。在这种情况下,我们可以通过在config 文件中设置一个特定的选项来增加大小。
例如,我们前面提到的8086例子需要1MB的内存用于虚拟机本身。除此之外,它还储存了一些数据和一个小型的编译器。因此,默认的1MB内存对于整个项目来说是不够的。
为了增加内存大小,我们必须在Rust项目目录下创建一个.cargo 目录,在我们的例子中,这个目录叫做rust 。接下来,在.cargo 目录中创建一个名为config 的文件。在这个文件中,我们可以指定分配给它的内存大小,如下图所示。
[target.wasm32-unknown-unknown]
rustflags = [
"-C", "link-args=-z stack-size=2000000",
]
在上面的代码中,我们指定堆栈大小为2MB,以字节为单位显示。现在,编译后的Wasm应用程序将被分配一个2MB大小的堆栈内存,而不是默认的1MB。
总结
现在你知道如何使用Wasm将Svelte应用程序连接到Rust库,以及如何从JavaScript中调用Rust编写的函数。你还可以从Rust中调用JavaScript函数,并在Rust-JavaScript边界上传递复杂的数据结构。
有了这些知识,你现在就可以编写既可用于网络又可用于桌面的库,或者制作一个快速的交互式界面来展示库的使用。