srtian / Blog

I just want to live while i'm alive
MIT License
3 stars 2 forks source link

浅谈 Rust 前端应用开发 #15

Open srtian opened 1 year ago

srtian commented 1 year ago

随着技术的不断演进,近年来愈来越多的非前端开发语言诸如:Rust、Go 等也开始进入前端/跨端应用开发领域,并收获了不小的开源社区的关注。因此本次在此尝试对基于 Rust 的一些 前端/跨端 应用开发进行一些分析,来分析一下这种开发模式的技术基础、基本方案等情况,由于篇幅原因将分为两部分:第一部分,主要讨论 Rust 前端应用开发的基本现状、原理以及收益等,而第二部分则会讨论号称 Electron 终结者的 Tauri 的基本情况、最大卖点、基本架构组成等。

一、兴起的基础

对于前端应用开发有一定了解的同学应该知道,可以在浏览器端运行的编程语言主要有两种:

而诸如 Rust、Golang 等这样的编程语言并不具备直接运行在浏览器端的能力,当今的 Rust 前端应用开发框架之所以能得到落地,最主要还是得益于 WebAssembly 近年来的快速发展。其主要的流程就是将 Rust 编译成 WebAssembly ,然后在浏览器端运行,大致如下:

因此讨论 Rust 来进行前端开发,WebAssembly 是无法绕开的一环,它的诸多特性也成为了 Rust/Go 等前端应用框架的卖点:

这几点特性也让使用 WebAssembly 开发前端应用在技术上已经成为了可能。而另一方面,Rust 由于其优秀的语言设计以及强劲的性能,已经多年稳坐最受欢迎语言的排名榜首,这让越来越多的开发者加入到了 Rust 学习以及开发的中去,让 Rust 技术社区不断壮大,也让诸多 Rust 开发者不断思考着其能够发光发热的领域。
而此时前端领域也遇到了自己的瓶颈,随着前端开发领域的不断扩展,前端所要实现的业务复杂度也在不断增长,这也对前端应用在性能以及安全性上提出了更高的要求。虽然经过多年来的不断改进,JavaScript 在性能上都得到了长足的进步,但相较于系统级语言还是存在较大的差距。而 Rust 作为一门系统级语言,在具有强大的性能的同时,还提供强大的所有权系统以及类型体系,对安全性提供了强力的保障,这无疑是对 JavaScript 极好的补充。于是一方是快速壮大的技术社区在寻求充分的发挥场景,一方是要求不断提升的前端领域,这二者的结合也就顺理成章了。

二、基本介绍

2.1、基本情况

在介绍 Rust 开发前端应用之前,需要先补充一个小点,现在使用 Rust 开发前端应用有两种开发方式,我将其称称为激进派以及改良派。其中激进派的做法是整个应用全部使用 Rust 进行开发,然后将其编译为 WASM 运行在 WebView 或者浏览器中,这也是我们今天所讨论的开发方式。而改良派则倾向于将应用的一部分用 Rust 进行开发,然后将其作为一个 Module 和前端应用进行组合,这也是一种较为常见的开发方式,由于篇幅问题在此就不做过多介绍。
总的来说现今的开源社区的 Rust 前端框架基本呈现一超多强的局面,大体上有以下几个较为出名的开源项目:

总的来看,这些框架在设计上,大体上具有几个比较共同的特性:

  1. 基于组件开发,虽然在具体写法上存在一些区别,但总的来说都是如此。
  2. 或多或少的都有些现代前端框架的影子,譬如:Yew 和 dioxus 之于 React,sycamore 之于 Svelte。
  3. 基本使用 virtual dom 的方式来对 dom 的操作进行的一定的抽象(毕竟 Wasm 不能直接操作 DOM)。

光说不练假把式,本着实践是检验真理的唯一标准,我周末花了些时间使用 dixous 实现了一个 TodoMVC 应用,感兴趣的同学可以直接点看下面的链接,试着运行项目看看:

https://github.com/srtian/todomvc-dioxus

从个人的开发体验上来看,由于 Rust 本身的语言设计和 JavaScript 有较大的区别,因此对于熟悉使用 JS/TS 的开发者来说,会有一定的 Gap,比如说状态管理时的区别;至于其他的诸如 JSX 等,由于这些框架都在借鉴 React 等前端框架的设计理念,所以总的来说差异并不大。

2.2、基本架构

至于这些 Rust 前端框架的大体架构,由于大体上都是借鉴了现代前端框架的开发模式,差距并不大,总的来讲都会有一个 html 的宏来负责对 virtual_dom 的处理以及映射,此外还会提供前端路由、状态管理、异步处理等能力,来提升框架的易用性以及能力。这里将以 dioxus 为例其主要构成大体如下:

正如之前所介绍的,dioxus 是基于 react 的风格所开发的前端框架,因此其中很多东西都和 react 生态系统保持了高度的一致:

mod components { mod link; mod redirect; mod route; mod router;

pub use link::*;
pub use redirect::*;
pub use route::*;
pub use router::*;

} pub use components::*;


- state 则主要是通过 hooks 来进行管理,按照其[官方描述](https://github.com/DioxusLabs/dioxus/tree/master/packages/hooks),主要有四个基础的 hooks:use_state、use_ref、use_future、use_coroutine。其中 use_state 和 use_ref 除了在使用以及能力上和 react 的 hooks 有些许区别外,其他的并无太大区别,而 use_future、use_coroutine 则主要用于处理异步的状态,具体使用场景可以移步官方文档对应的[章节](https://dioxuslabs.com/guide/async/index.html),写得很详细,在此就不做过多的赘述。
- 然后就是 Virtual DOM 了,这也是几乎所有 Rust 前端框架的重中之重。一方面由于 WebAssembly 操作 DOM 比 JavaScript 具有更高的成本,因此需要使用 Virtual DOM 来减少 DOM 的操作频率,以提升性能;其次在跨端/服务端渲染部分, Virtual DOM 也有着非常重要的作用。因此dioxius 的 Virtual DOM 在借鉴了 react 的相关优秀理念的同时,还借鉴了 [Dodrio](https://github.com/fitzgen/dodrio) 诸如:Bump Allocation、Change List as Stack Machine 等等设计思想以提升其 Virtual DOM 的性能以及内存使用效率;最后还充分利用的 Rust 所有权的特性对内存进行优化。从而做到:通常情况下一旦加载了应用,就不再需要执行分配操作,只有当新组件被添加至 dom 中时,才会进行再分配;且对于给定的组件,添加新节点时,会动态的回收旧的虚拟DOM的空间;最后还会记录之前的组件的平均内存占用情况,从而预估未来组件需要分配多少内存。
- 最后 dioxus 提供了 rsx! 和 html! 这两个宏来为开发者提供类似于 JSX 的开发功能,本质上主要的能力就是将我们所写的 html/rsx 转化成 Virtual DOM ,没个元素则有下列属性组成:
```rust
#[derive(PartialEq, Eq)]
pub struct Element {
    pub name: Ident,
    pub key: Option<LitStr>,
    pub attributes: Vec<ElementAttrNamed>,
    pub children: Vec<BodyNode>,
    pub _is_static: bool,
}

至于在跨端部分,dioxus使用的实际还是 Tauri 所提供的 wry 来进行 WebView 的侨接。这块儿在下一部分来讲,再次就不做过多赘述了。

2.3、基本分析

1、性能

Rust 作为系统级的编程语言,在性能上的优势是毋庸置疑的,swc、postcss-rs 等工具的兴起最大的原因就在于此。譬如postcss-rs 就给出的性能对比:

从上图的直观表现来看,rust 在性能上较之 JS 具有非常大的优势。但这里的性能差距并不能作为讨论的前端应用开发场景下的依据。前面也提到过 Rust 并不能直接运行在浏览器端,需要编译成 WebAssembly 才能运行在浏览器端,所以对比的对象应该是 WebAssembly 和 JavaScript。
而在Wasm方面,我们前面曾提到,虽然 WebAssembly 在性能上相较于 Javascript 有一定优势,但由于无法直接操作DOM,所以并不一定会在前端应用上有很好的表现,这也是很多早期 Wasm 用户所吐槽的点。但这个问题也只是暂时的,Interface Types 计划完全解决这个问题,且随着 WebAssembly 的不断发展,这一情况也会得到改善,我们可以直接使用框架之间的 benchmark 来进行对比(这里现在还不支持 dixous 进行对比,因此选用了 yew、sycamore 以及 wasm-bindgen ):

可以看到,Rust 系的前端框架在常见的DOM操作方面的性能上大多和传统的前端框架没有明显的区别,甚至在一些场景下要优于 Angular 和 React。而在其他方面也表现很不错:

因此在性能上来讲,Rust 系的前端框架还是保持着一个较为不错的表现的,并没有出现所谓的 wasm 在 dom 操作上性能非常差的现象。而对于计算密集的场景,WASM 则要优于 JS,具体讨论可以看下述文章,在此就不再做过多的讨论了:
how-fast-and-efficient-is-wasm
总的来说,虽然WASM在操作DOM的性能上仍然存在较大的进步空间,但距传统前端框架的性能差距并不大;而在计算密集的场景下,则会有一定的优势。

2、安全

基于 Rust 开发的应用在安全方面无疑也是具有竞争力的。首先 Rust 本身由于所有权系统以及生命周期来实现内存管理,没有运行时的GC,加之强大的编译器的代码检查,都为构建构建内存安全的应用提供了坚实的保障。
而对于 WebAssembly 来说,正如它自身的文档所说的,WebAssembly 的安全模型主要有两个目标:

  1. 不让用户遭受 Bug 以及恶意模块的影响
  2. 为开发者提供足够的能力开发安全的应用

首先在内存方面 WebAssembly 只提供一个沙盒化的线性内存(linear memory),致使其对内存的访问十分有限,只能对这个线性内存进行读写以及扩缩容,无法对其他内存进行操作。这虽然损失了一定的便利性,但在内存安全方面也提供了一些保障,对于 WASM 内存安全的详细介绍,可以参考下列文章:
https://hacks.mozilla.org/2017/07/memory-in-webassembly-and-why-its-safer-than-you-think/
而在控制访问方面。WASM 也做的非常的好:WebAssembly 代码本身是在一个由虚拟机管理的沙盒中封闭运行的,这让它与主机是相互隔离的,无法与主机直接进行交互。在这种情况下,如果想实现对系统资源的访问就只能通过虚拟机所提供的 WebAssembly 系统接口(WASI)来进行。而WASI 提供了基于能力的安全模型(Capability-based security),遵循最小权限原则,譬如在进行指定文件等资源的访问时,需要显示的在外部传入加有权限的文件描述符的引用,对于其他未授权的资源是无法访问的,这种依赖注入的方式可以避免很多传统安全模型的潜在风险。
总的来说,在安全方面 Rust + WebAssembly 的组合能帮助我们写出更加安全的应用,为用户的安全保驾护航。

三、总结 && 展望

综上所述,Rust 开发前端应用开发主要是通过将代码编译为 WebAssembly 从而实现在浏览器端运行甚至是跨端的目的。但需要注意的是,站在2022年的来看,这种开发模式仍然还是稚嫩的,存在不少的问题:

  1. 开发团队的搭建成本,虽然上文提到 Rust 已多年连续蝉联最受欢迎编程语言的榜首。但它学习曲线的陡峭性仍然会让很多开发者望而却步,因此当一个应用选择使用这种方式进行开发时,会需要搭建一个 Rust 开发团队,但显然搭建一个前端工程师团队还是会比搭建一个 Rust 开发团队要迅速、简单不少。
  2. 开发速度,前端场景下,大部分的业务强调的还是快准狠,现代前端框架也是向着这个方向发展的,而 Rust 在开发业务的速度上明显是很难与 React/Vue 相抗衡。

  1. 生态系统的问题,虽然 Cargo 生态经过近几年的快速发展,已经到达了一个较为不错的水平,但要和广大的前端工程师的生态系统比,仍然存在一定差距,还需要一定的时间来进行追赶。

也正是由于以上原因,以 Rust 为主力语言进行前端应用开发的方式并没有取得很多的落地。取得落地的反而是一些混合应用,比较典型的就是 Figma:将部分计算密集或者是业务逻辑复杂的模块使用 Rust/Cpp 进行开发,在这些模块内实现计算逻辑,对外暴露出计算结果/指令,然后通过 canvas 去对页面进行绘制,这样可以有效的缓解 WebAssembly 操作 DOM 所带来的性能损耗从而保障性能。至于整体都使用 Rust 进行开发,我想还需要 WebAssembly 得到足够的普及以及得到更好的发展,才能实现。
好了,本文对 Rust 进行前端应用开发进行了一个简单的介绍,下文将开始介绍号称 Electron 杀手的 Tauri,看它是如果利用我们在本文做介绍的这些基础能力,成为当今炙手可热的客户端应用开发方案的。

参考资料

xujiahao commented 1 year ago

感谢分享!