走进 WebAssembly 和 Rust 的编程世界
在进行正式的分享之前,先来说一说为什么,要学习 Rust 这一门在广义上归属于后端的语言,以及它能带给我们什么,未来有什么前景。
-
与JavaScript部分相似的语法,就入门来说,应该不难(大概) -
安全高效的新兴语言,通过Rust你可以对计算机的底层是如何操作的有一个基本的认识。 -
依托于WebAssembly,Rust可以运行在浏览器上,在某些场景(如视频直播或需要大量运算)具有卓越的性能,例如我们经常用的figma就有使用到WebAssembly。 -
掌握至少一门后端语言有助于后续的提升,Node.js也很对,但是对于计算机底层相对于cpp和rust较黑盒。 -
Rust的设计哲学值得一看。
Rust
Rust 语言是一种高效、可靠的通用高级语言。其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言。Rust 是一种 预编译静态类型(ahead-of-time compiled)语言,这意味着你可以编译程序,并将可执行文件送给其他人,他们甚至不需要安装 Rust 就可以运行。如果你给他人一个 .rb、.py 或 .js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM)。
中文学习资源 https://kaisery.github.io/trpl-zh-cn/ch01-01-installation.html
以上摘抄自官方文档等学习资源 下面不逐个介绍Rust的语法与编译方式,主要介绍一些我认为的Rust语言的一些有意思的特点与设计思想。
-
Rust中变量默认是不可变的(immutable)变量不可变可以说是一种规范,可以帮助我们更加直观的追寻数据的变化状态。例如使用react的PureComponent 或者 memo只会对行新旧数据的浅层比对,由于 JS 引用赋值的原因,这种方式仅仅适用于无状态组件或者状态数据非常简单的组件,对于大量的应用型组件,它是无能为力的,所以在编写的时候会考虑使用immutable +memo 浅层比对。
举个最简单的例子
// 可变,arr新增
arr.push(item)
// 不可变
arr = [...arr,item]
-
完善的类型系统(和Typescript极其相似),但是在某些方面比Typescript更加的细,例如整形可以分为无符号,有符号的8-bit,16-bit,32-bit,64-bit,128-bit。
// 无符号的32位整形
let guess: u32 = "42".parse().expect("Not a number!");
-
语言的集大成者,既有Javascript的灵活,又有C/C++的编译加持。
举一个体现其灵活的例子
let x = 5;
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
在上面的例子中Rust使用;来判断该句子是表达式,还是一个语句。
-
严谨的,灵活的控制流
// 会报错
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
// 形如以下的赋值语句是完全有效的
let condition = true;
let number = if condition {
5
} else {
6
};
-
独特的内存管理方式,区别于垃圾回收机制(javascript)和亲自分配和释放内存(C/C++),Rust采用了另外一种管理操作系统内存的方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
如果内存的操作分为以下两部分:
-
必须在运行时向操作系统请求内存。 -
需要一个当我们处理完 String 时将内存返回给操作系统的方法。
第一步大同小异,而对于第二步的处理就是百花齐放了,对于Rust而言
Rust 中的每一个值都有一个被称为其 所有者( owner)的变量。 值在任一时刻有且只有一个所有者。 当所有者(变量)离开作用域,这个值将被丢弃。
为了保持运行时的高效,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。对于此,Rust采用了一个规则,禁止把引用堆空间的栈空间变量改变(栈空间上的值类型可以直接引用),因为Rust 不需要在被首次分配空间的变量离开作用域后清理任何东西。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
// 会报错
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
// 需要手动克隆
那么当所有者(变量)离开作用域,这个值将被丢弃这句话怎么理解呢?看下面的图,这种操作既不像浅拷贝(shallow copy)和 深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),但是注意一点对于值类型,Rust会直接拷贝,而不是进行移动,所以对于值类型(整形等),有函数调用它之后,仍然可以使用。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢?这里就需要引用和借用(可以理解为c里面的指针)。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
// 尝试修改,那指定是不行的
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
函数调用了引用类型的引用,在函数体中使用该变量被称之为借用,那么又有一个问题了,你不让我改,我就是想改,诶,就是玩!那么这时候需要引入一个新的概念:可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
不过可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用,并且可变引用和不可变引用不应该同时存在(这两是互斥的关系)。
// 会报错
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
// 会报错 too
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);
这个限制的好处是 Rust 可以在编译时就避免数据竞争,可以理解为类似于分布式锁的玩意儿~ 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码 注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以如下代码是可以编译的:
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
但是虽然可以编译,这样书写是绕不过静态类型检查的!!!!!!
相信大家发现了上面的string类型有些特殊,不是说string是"值类型"吗?为什么他又可以用引用类型来表示呢?string使用了没有所有权的特殊的引用类型slice,slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
对于"值类型"的string
let s = "Hello, world!";
这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。
悬垂引用 悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态 例如以下代码:
fn main() {
let reference_to_nothing = dangle();
}
// wrong
fn dangle() -> &String {
let s = String::from("hello");
&s
}
//safe
fn safe() -> String {
let s = String::from("hello");
s
}
因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!Rust 不会允许我们这么做 总结两条规则
-
在任意给定时间, 要么 只能有一个可变引用, 要么 只能有多个不可变引用。 -
引用必须总是有效的。
到这里和JavaScript有联系的,并且基础的就分享的差不多了,隐约记得鲁迅说过,如果你对一门语言,了解了其基本的语法,能够编写对应的简单的代码来实现简单的功能,那么你就入门了。后续的包括以下部分,就先按下不表
-
Cargo : Cargo 是 Rust 的构建系统和包管理器。大多数 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。类似于JS使用的npm/pnpm/yarn -
常见集合:Hashmap(类似于js中的map),Vector(类似于js中的数组),String -
错误处理:panic(Throw Error 完全阻塞了程序执行) Result(类似于warning 可以报错但是不影响程序的执行) -
.......
最后总结一下rust我认为最令人称道的两点
-
丰富而强大的类型系统 -
可信赖的所有权模型
Rust and WebAssembly
上面讲了半天rust,他只是我们今天的猪脚之一,那么今天的猪脚还有哪位呢?没错,就是 WebAssembly。那么WebAssembly到底是什么呢?在说这个之前先康康JavaScript的是怎么进行编译的 这就不得不说到两种编译方式了
-
AOT: Ahead-of-Time compilation
必须是强类型语言,编译在执行之前,编译直接生成CPU能够执行的二进制文件,执行时CPU不需要做任何编译操作,直接执行,性能最佳,比如C/C++,Rust
-
JIT: Just-in-Time compilation
没有编译环节。执行时根据上下文生成二进制汇编代码,灌入CPU执行。JIT执行时,可以根据代码编译进行优化,代码运行时,不需要每次都翻译成二进制汇编代码,V8就是这样优化JavaScript性能的。
举个例子,如果使用var来声明一个变量,不使用Typescript等类型系统来限定,一个变量,在多次编译的时候得到的变量的类型可能会不一样,这就导致了每一次JavaScript在执行的时候可能都会被重新编译,这就是类型系统的重要性,不仅能减少bug的发生也可以让我们的代码跑得更快
详细的说一下这个过程也就是
-
代码文件会被下载下来。 -
然后进入Parser,Parser会把代码转化成AST(抽象语法树). -
然后根据抽象语法树,Bytecode Compiler字节码编译器会生成引擎能够直接阅读、执行的字节码。 -
字节码进入翻译器,将字节码一行一行的翻译成效率十分高的Machine Code.
有同学可能会问:JavaScript不是可以使用Typescript进行静态类型检查吗?为什么不能在编译时编译成可执行的二进制文件呢?盲生,你发现了华点!Typescript说白了也只是给JavaScript打上了补丁,但是JavaScript还是那个JavaScript,说不定在有生之年可以看见JavaScript的整个内核被重写呢?Wasm:那我走?
回到正题,既然JavaScript的内核变化的几率不大,那我们该如何进行优化呢?一个思路就是可以直接把 C、C++、Rust等语言编译成 WebAssembly 并能在浏览器中运行,但是有一点需要注意,使用wasm并不是完全舍弃掉了JavaScript,这两者实际上是相辅相成的关系,在实际的应用场景中Rust和JavaScript往往是互相调用包来开发一个web应用。
WebAssembly是一份字节码标准,以字节码的形式依赖虚拟机在浏览器中运行。万维网联盟(W3C)2019年12月5日宣布,WebAssembly 核心规范 现在是一种正式的 Web 标准,它为 Web 发布了一种功能强大的新语言。WebAssembly 是一种安全、可移植的低级格式,能够在现代处理器(包括 Web 浏览器)中高效执行并紧凑地表示代码。它也被设计为可以与JavaScript共存,允许两者一起工作。这样说大家可能云里雾里的,那么换个方法 我们每天都在接触各种业务,那大家有没有想过从我们写下JavaScript代码开始,到底发生了什么?就只看JavaScript大致是这样一个过程:
业务代码 -> v8 解析 -> 得到编译结果(字节码) -> 线程通信 -> 通知GPU绘制 -> 渲染
那如果我们使用了WebAssembly,那又是一个什么过程呢?
业务代码 -> 编译 -> 字节码 -> 线程通信 -> 通知GPU绘制 -> 渲染
可以看出,这两个链路最大的区别就是,在第二种链路中,浏览器(V8)所得到的东西,已经是一份可以执行的字节码了,他只需要执行就完事了,而不需要使用大量的CPU来对可能很复杂的源代码来进行编译。(当然也可以使用worker 这里就不做讨论了) 但是纯纯的字节码指定是不行的,C/C++,Rust可能都有自己的一套规范,所以这就需要一套规范来整合一下,让大家都可以愉快的在浏览器中玩耍,这可以说就是WebAssembly,由他的标准可以生成后缀名为.wasm的文件,可以直接交给浏览器执行 目前主流的浏览器都已经支持了WebAssembly。除此之外 ,依照wasm的特性,个人认为或者wasm未来在多端也能有一定的用处
实战
俗话说的好,纸上得来终觉浅,绝知此事要躬行,上面简单学习了rust+wasm,那如果不实践一下那不是浪费了吗,那到底怎么实践rust+wasm呢?自己看着wasm的文档写?那指定是不行的。那怎么办呢?不要慌,今天的第三位猪脚出现了:Yew 文档在此yew中文文档简介如下Yew 是一个设计先进的 Rust 框架,目的是使用 WebAssembly 来创建多线程的前端 web 应用。
-
基于组件的框架,可以轻松的创建交互式 UI。拥有 React 或 Elm 等框架经验的开发人员在使用 Yew 时会感到得心应手。 -
高性能 ,前端开发者可以轻易的将工作分流至后端来减少 DOM API 的调用,从而达到异常出色的性能。 -
支持与 JavaScript 交互 ,允许开发者使用 NPM 包,并与现有的 JavaScript 应用程序结合。
让一个yew应用跑起来分三步(确信)
-
创建一个二进制项目
cargo new --bin yew-app && cd yew-app
-
编写代码,注意要编写index.html -
启动
cargo install trunk wasm-bindgen-cli
rustup target add wasm32-unknown-unknown
trunk serve
一张图简述一下wasm-bindgen的作用
组件化
页面展示的代码
use yew::prelude::*;
enum Msg {
AddOne,
}
struct Model {
link: ComponentLink<Self>,
value: i64,
}
impl Component for Model {
type Message = Msg;
type Properties = ();
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
value: 0,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::AddOne => {
self.value += 1;
true
}
}
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<p>{ "tandake is a Vegetable Chicken" }</p>
<button onclick=self.link.callback(|_| Msg::AddOne)>{ "+1" }</button>
<p>{ self.value }</p>
</div>
}
}
}
fn main() {
yew::start_app::<Model>();
}
效果演示
可以看出,这里的渲染的文件来源已经是wasm了 眼尖的同学可能已经发现了上面的create,update ,change几个函数,那么他们是用来干嘛的呢?简单说一下yew中组件的生命周期:Component 特质定义了六个生命周期函数。
-
create 是一个构造函数,接收道具和ComponentLink -
view 渲染该组件 -
update 当一个Message 被发送到该组件时被调用,实现消息传递的逻辑 -
change 重新渲染变化,优化渲染速度 -
rendered 在view 之后但在浏览器更新之前被调用一次,以区分第一次渲染和连续渲染。 -
destroy ,当一个组件被卸载并需要进行清理操作时被调用。
如果把他类比成react的类组件,那么create就是constructor构造函数,update就是相当于注册在组件内部的一些静态方法,change相当于shouldcomponentupdate,其他的生命周期也可同比
父子组件中通信
前文说到yew是基于组件的,那么父子组件该怎么进行最简单的数据通信呢?声明父组件
#[derive(Clone, PartialEq, Properties, Default)]
struct Properties {
name: String,
}
enum Message {
ChangeName(String),
}
struct Model {
link: ComponentLink<Self>,
props: Properties,
}
impl Model {
fn change_name(&mut self, name: String) {
self.props.name = name;
}
}
impl Component for Model {
type Message = Message;
type Properties = Properties;
fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
props: Properties {
name: "tandake".to_string(),
},
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Message::ChangeName(name) => {
self.change_name(name);
}
};
true
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<p>{ "大家好,我是练习时长两天半的rust实习生,谭达科" }</p>
<p>{"hello "}{self.props.name.clone()}</p>
<Button onclick={self.link.callback(|name: String| Message::ChangeName(name))} />
</div>
}
}
}
声明子组件
#[derive(Clone, PartialEq, Properties, Default)]
struct ButtonProperties {
onclick: Callback<String>,
}
enum ButtonMessage {
ChangName,
}
struct Button {
props: ButtonProperties,
link: ComponentLink<Self>,
}
impl Button {
fn change_name(&mut self) {
self.props.onclick.emit("is a vegetableChicken".to_string());
}
}
impl Component for Button {
type Message = ButtonMessage;
type Properties = ButtonProperties;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { props, link }
}
fn update(&mut self, msg: Self::Message) -> bool {
match msg {
ButtonMessage::ChangName => {
self.change_name();
}
};
true
}
fn change(&mut self, props: Self::Properties) -> bool {
if self.props != props {
self.props = props;
true
} else {
false
}
}
fn view(&self) -> Html {
html! {
<button onclick={self.link.callback(|_| ButtonMessage::ChangName)}>{"click me"}</button>
}
}
}
演示效果
声明一个组件需要一些什么东西呢?从上面这个简单的demo可以看出一个大概
-
定义属性结构
#[derive(Clone, PartialEq, Properties, Default)]
-
把属性附加到状态
struct Button {
props: ButtonProperties,
link: ComponentLink<Self>,
}
-
初始化组件的状态
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { props, link }
}
-
初始化生命周期,在update中接受事件,在change中重新渲染
fn update(&mut self, msg: Self::Message) -> bool {
match msg {
ButtonMessage::ChangName => {
self.change_name();
}
};
true
}
fn change(&mut self, props: Self::Properties) -> bool {
if self.props != props {
self.props = props;
true
} else {
false
}
}
fn view(&self) -> Html {
html! {
<button onclick={self.link.callback(|_| ButtonMessage::ChangName)}>{"click me"}</button>
}
}
-
需要交互注册自定义事件
impl Button {
fn change_name(&mut self) {
self.props.onclick.emit("is a vegetableChicken".to_string());
}
}
这种写法与现在我们使用的react的写法是比较类似的(当然也可以使用vue的emit的方式)
函数式组件
上诉yew的组件多多少少和类组件比较像,那么yew可不可以使用一种类似函数式组件的方法?甚至使用hooks呢?当然可以 下面我们来实现一个简单的点击计数器(效果和第一个类似,就不再赘述了)
#[derive(Properties, Clone, PartialEq)]
pub struct RenderedAtProps {
pub time: String,
}
#[function_component(App)]
fn app() -> Html {
let (counter, set_counter) = use_state(|| 0);
let onclick = {
let counter = Rc::clone(&counter);
Callback::from(move |_| set_counter(*counter + 1))
};
html! {
<div>
<button onclick=onclick>{ "Increment value" }</button>
<p>
<b>{ "Current value: " }</b>
{ counter }
</p>
</div>
}
}
#[function_component(RenderedAt)]
pub fn rendered_at(props: &RenderedAtProps) -> Html {
html! {
<p>
<b>{ "Rendered at: " }</b>
{ props.time.clone() }
</p>
}
}
这样看起来是不是有react的函数式组件那味了,在使用函数组件的时候,我们也可以使用yew中自带的各种hooks,包括了但不限于以下hook钩子
-
use_state -
use_ref -
use_reducer -
use_reducer_with_init -
use_effect -
use_effect_with_deps
Yew虽然说是一款Rust框架,但是在实际使用上与Rust相关,而与我们现在学习的知识无关的地方很少,大多的时候我们都在这里面看到vue和react中的影子,使用的成本其实并不高,就像我们使用JavaScript来开发一样,大家都知道JavaScript是基于V8的,但是我们在编程的时候不是只是关注V8来进行开发吧?这个框架也是一样,虽然基于WebAssembly和Rust,但是使用起来,会比我们想象的顺滑很多。
WebAssembly 和 Javascript
上面讲了yew这个新框架,但是问题又来了,这不是还是要学习Rust吗?我不会Rust,但是我就是想用WebAssembly!我就是想用JavaScript!那怎么办呢?没事,你能想到的,大家都想到了,那下面又来了一位猪脚。
AssemblyScript:用Javascript的方式来编写WebAssembly
还记得上面在介绍rust的时候,提到过的的Rust比Typescript更加丰富的系统吗?是不是看的心痒痒?没事,Rust的类型系统的确很好,但是下一秒就是我的了,那下面再请出一位猪脚 AssemblyScript 看一句官网的描述
AssemblyScript compiles a variant of TypeScript(a typed superset of JavaScript) to WebAssembly using Binaryen,It generates lean and mean WebAssembly modules while being just an npm install away
// 😢
var a = {}
a.prop = "hello world"
// 😊
var a = new Map<string,string>()
a.set("prop", "hello world")
// 😢
function foo(a?) {
var b = a + 1
return b
}
// 😊
function foo(a: i32 = 0): i32 {
var b = a + 1
return b
}
// 😢
function foo(a: i32 | string): void {
}
// 😊
function foo<T>(a: T): void {
}
当然,目前这门语言还有许多不完善的地方,对应的生态也不成熟,这门语言的目标是想要成为一个对web开发者上手门槛极低的语言,但是同时他又是一门最终需要编译为WebAssembly的语言,这就需要它在支持目前已有的开发语言的特性的基础上,又不能在依然保有某些语言编译效率底下的特性或者是盲目的迷信二进制的路上越走越远,这可以说是这门语言的哲学,也可以说是这门语言前进的方向 具体的用法就不多说了,大家有兴趣的可以去研究一下(手动狗头)AssemblyScript官方英文文档
整合 WebAssembly+Javascript+Vite+Vue/React+Rust
其实分享到这里,看了wasm这么多骚操作之后,又想起了我们平时的开发框架Vue/React,自然而然,我就会想到:那我能不能直接在Vue/React中使用WebAssembly呢?再过分一点,我甚至把下一代打包构建工具Vite也用上?答案只有一个:可以 下面我们来实战操作一下,以Vue3为示例 使用到的技术名词
-
Vite: 下一代前端工具 -
Rust: 一门赋予每个人构建可靠且高效软件能力的语言 -
WebAssembly: WebAssembly(缩写为Wasm)是基于堆栈的虚拟机的二进制指令格式。Wasm被设计为编程语言的可移植编译目标,从而可以在Web上为客户端和服务器应用程序进行部署。 -
wasm-pack: Rust→Wasm 工作流程工具! -
vite-plugin-rsw: Vite插件,集成了wasm-pack的CLI,生成wasm的npm包,实现了文件变更,自动构建及热更新。
第一步:安装依赖,并且配置vite
# install rsw
npm i -D vite-plugin-rsw
# or
yarn add -D vite-plugin-rsw
// vite.config.ts
import { defineConfig } from 'vite';
import ViteRsw from 'vite-plugin-rsw';
export default defineConfig({
plugins: [
ViteRsw({
crates: [
'@rsw/hey',
'rsw-test',
// https://github.com/lencx/vite-plugin-rsw/issues/8#issuecomment-820281861
// outDir: use `path.resolve` or relative path.
{ name: '@rsw/hello', outDir: 'custom/path' },
],
}),
],
});
第二步
-
配置rust
use wasm_bindgen::prelude::*;
// Import the `window.alert` function from the Web.
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
// Export a `greet` function from Rust to JavaScript, that alerts a
// hello message.
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
-
在JavaScript的模块中引用
<template>
<button @click="greet('webAssembly')">hello wasm</button>
<button @click="greet2('wasm')">hi wasm</button>
</template>
<script lang="ts">
import init, { greet } from '@rsw/hey';
import init2, { greet as greet2 } from 'rsw-test';
import { ref, defineComponent } from 'vue'
// init wasm
init();
init2();
export default defineComponent({
name: 'HelloWasm',
setup: () => {
return { greet, greet2 }
}
})
</script>
当然,这样本质上还是rust不过是在vite+vue3的环境中来书写rust,利用wasm-bindgen让rust和JavaScript可以互相调用,体验上确实没有JavaScript来得好,不过也算是另一种开发的思路。
总结
不能说未来全是WebAssembly,但是以后WebAssembly在web上绝对会有一个极其重要的地位,现在的WebAssembly各种思路,百花齐放,但是有一点是不变的,WebAssembly的发展一定是紧跟着未来的趋势的,各种WebAssembly的GitHub仓库,各种构建打包工具,各种WebAssembly的开发框架,都如雨后春笋一般正在涌现。当然Rust也是一样,我相信这俩兄弟,在未来一定大放光彩!