Rust 在前端领域的应用

什么是 Rust?

Rust 语言原本是 Mozilla 员工 Graydon Hoare 的私人项目,Mozilla 于 2009 年开始赞助这个项目,在 2010 年官方首次透露并于 2015 年发布 1.0 版本。在过去的十年中,编程语言 Rust 一直是一项突破性的技术,Rust 始终站在独特的学术研究和行业实用性结合的挑战视角。但是如果说 Rust 的影响仅仅是技术性的,那就错过了精髓,正如社区在 2016 年讨论的系列博客一样:"Rust 不仅是编程语言或编译器"和"Rust 让一切触手可及",同年 Rust 宣布了其官方口号:“一种让每个人都能够构建可靠和高效软件的语言”

2021 年 2 月 8 日,Rust 基金会宣布成立,其基金会董事成员有:AWS、Google、华为、微软、Mozilla 。Rust 基金会诞生自 Rust 核心团队,并且得到了五位全球行业领先公司的财务承诺,这标志着 Rust 向成熟化迈出了坚实的一步。

为什么是 Rust?

性能

各种编程语言内存管理的方式不同,但通常有以下两种方式:

  1. 开发者自己分配和销毁: 比如 C、C++ 等,这种方式相当于把所有权力开放给开发者,管理不当容易内存泄漏。
  2. 编程语言提供自动垃圾回收机制: 比如 JavaScript、Java、Python 等,这种方式会产生运行时开销,对性能可能产生影响(注意这里是“可能”,没有办法证明性能一定比开发者自己管理要差)。

Rust 则另辟蹊径采用所有权、借用、生命周期机制在编译期自动插入内存释放逻辑来实现内存管理,由于没有了垃圾回收产生的运行时开销,Rust 整体表现的速度惊人且内存利用率极高。

fn main() {
   let a = String::from("hello rust");
   let b = a;           // 所有权被转移
   println!("{}", a);  // 编译失败!a 已经被释放,无法再使用
}

在一项比较 REST API 性能的基准测试中(Rust 使用 Rocket,Node.js 使用 Restify),Rust 每秒处理 72,000 个请求,而 Node.js 为 8,000 个,空闲时使用的内存大约为 1MB,而 Node.js 为 19MB。 在另一个测试中(Rust 使用 Nickel,Node.js 使用 Restana),Rust 对请求的平均响应速度比 Node.js 快近 100 倍。

具体数据可以参考 Web Frameworks Benchmark

可靠性

Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误,关于“内存安全”和“线程安全”的解释如下:

内存安全: 在具有内存安全性的编程语言中,所有内存访问都是明确定义的,通常内存不安全的情况包含:空指针、野指针、悬空指针、使用未初始化的指针、非法释放、缓冲区溢出、执行非法函数指针、数据竞争等。

据说微软 70% 的漏洞是内存安全问题

线程安全: 线程安全是程序设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

生产力

Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。

Rust 在前端构建工具中的应用

Deno

Github((80.9K star):https://github.com/denoland/deno

Deno 是一个简单、现代且安全的 JavaScript 和 TypeScript 运行时,它使用 V8 并基于 Rust 构建。Deno 是由 Node.JS 之父 Ryan Dahl 创建,在 2018 JS Conf Berlin 上借演讲《Design Mistakes in Node》首次对外公开。

deno

Deno 诞生之初就是为了解决 Node 不安全和糟糕包的管理等老生常谈的问题,其中不安全时常令 Node.JS 开发者感到头疼和愤怒,近期也刚刚发生 node-ipc 事件(node-ipc 在所有用户的桌面上都会创建一个文件来宣传作者的政治观点),影响到了众多开源项目包括 Vue CLI 等。

vue-cli

因此 Deno 很自然地拥有以下特性:

  1. 默认安全,除非特别启用它,否则使用 Deno 运行的程序没有文件、网络或环境访问权限

    deno run --allow-read mod.ts
    
  2. 开箱即用地支持 TypeScript

  3. 仅编译单个可执行文件

  4. 拥抱 Web 生态标准,内置了 fetch、localStorage、location 等 API

    localStorage.setItem("myDemo", "Deno App"); // 拥有 10M 的持久化存储限制
    
  5. 内置依赖检查器 deno info 和代码格式化工具 deno fmt

  6. 有一组经过审查的标准模块,可以与 Deno 一起使用:deno.land/std

目前已经有众多公司正在积极探索 Deno,包括 Amazon、Github、IBM、Vercel、Tencent、Microsoft 等头部技术公司。

SWC

Github(21K star):https://github.com/swc-project/swc

SWC 是一个可扩展的基于 Rust 的前端构建工具,目前核心功能相当于 Babel,包含以下这些模块:

模块状态作用
@swc/cli稳定该模块提供命令行入口
@swc/core稳定该模块提供核心 SWC API
@swc/wasm-web稳定该模块允许使用 WebAssembly 在浏览器内同步转换代码
@swc/jest稳定ts-jest 的代替品,可以使单元测试速度有质的提升
swc-loader稳定该模块允许将 SWC 与 Webpack 一起使用
swcpack建设中该模块试图提供完整的构建器模块,可以简单理解为 Rust 版的 Rollup 或 Webpack

官方提供的基准测试数据如下:

name1 core, sync4 promises100 promises
swc (es3)616 ops/sec1704 ops/sec2199 ops/sec
swc (es2015)677 ops/sec1688 ops/sec1911 ops/sec
swc (es2016)1963 ops/sec3948 op s/sec5580 ops/sec
swc (es2017)1971 ops/sec3948 ops/sec6259 ops/sec
swc (es2018)2555 ops/sec4884 ops/sec8108 ops/sec
swc-optimize (es3)645 ops/sec1716 ops/sec1860 ops/sec
babel (es5)34.05 ops/sec27.28 ops/sec32 ops/sec

SWC 在单线程上比 Babel 快 20 倍,在四核上快 70 倍。

目前 SWC 已经被 Next.js、Parcel 和 Deno 等工具以及 Vercel、字节跳动、腾讯、Shopify 等公司广泛使用。

Parcel

Github(40k star):https://github.com/parcel-bundler/parcel

支持以 HTML 作为入口的零配置构建工具,Parcel 支持多种开箱即用的语言和文件类型,从 HTML、CSS 和 JavaScript 等 Web 技术到图像、字体、视频等资产。当您使用默认不包含的文件类型时,Parcel 将自动为您安装所有必要的插件和开发依赖项。Parcel 的 JavaScript 编译器和源映射是建立在 SWC 编译器之上的,在 SWC 之上,Parcel 实现了依赖项收集、捆绑、摇树优化、热重载等。

目前 Parcel 已经被广泛应用在微软、Atlassian、SourceGraph 等公司。

Rome

Github(17.2 star):https://github.com/rome/tools

Rome 是 Babel 作者做的基于 Node.js 的前端构建全家桶,包含但不限于 JavaScript、TypeScript、JSON、HTML、Markdown 和 CSS,在 2021 年 9 月 21 日 宣布计划使用 Rust 重构。

其它工具(WIP)

dprint:使用 Rust 编写,比 Prettier 快 30x 倍

postcss-rs:使用 Rust 编写,比 Postcss 快 20x 倍

Rust 在桌面应用开发中的应用

Tauri

Github(34.7 star):https://github.com/tauri-apps/tauri

在很长一段时间里,包括现在,Electron 都是最流行的跨平台桌面应用开发框架,目前在 Github 上有 101K 个 star,它允许你使用纯粹的前端技术(HTML、CSS、JS、Node.JS)来构建桌面应用,不过它也有两个比较明显的缺陷被人诟病:包体积太大和内存占用高,而造成这两个问题的根本原因是 Electron 是基于 Chromium 和 Node.JS 构建的。

Tauri 是 Electron 的代替品,现在 Tauri 试图去除 Chromium 转而使用 Rust 去和系统内置 Webview 进行绑定,简单来说就是在 Electon 时代你的应用永远使用 Chromium 内核,而在 Tauri 时代,你的应用在 Windows 上使用 Edge/Webview2,在 macOS 上使用 WebKit,在 Linux 上使用 WebKitGTK。基于 Rust 和 Webview 的好处很明显:包体积极小且内存占用极低。以下是官方提供的数据:

DetailTauriElectron
Installer Size Linux3.1 MB52.1 MB
Memory Consumption Linux180 MB462 MB
Launch Time Linux0.39s0.80s
Interface Service ProviderWRYChromium
Backend BindingRustNode.js (ECMAScript)
Underlying EngineRustV8 (C/C++)
FLOSSYesNo
MultithreadingYesYes
Bytecode DeliveryYesNo
Multiple WindowsYesYes
Auto UpdaterYesYes1
Custom App IconYesYes
Windows BinaryYesYes
MacOS BinaryYesYes
Linux BinaryYesYes
iOS BinarySoonNo
Android BinarySoonNo
Desktop TrayYesYes
Sidecar BinariesYes

看起来还不错的样子,不过别忘了如果你使用 Tauri 开发的话,后端(Electron 中叫主进程)目前只能使用 Rust,这将带来不小的学习成本,除此之外 Tauri 还有很长的路需要走,不过也算文艺复兴式的创新。

Rust 在 WebAssembly 中的应用

WebAssembly 是一种新的编码方式,具有紧凑的二进制格式,可以在现代的网络浏览器中以接近原生的性能运行,目前 Rust 和 WebAssembly 结合有两大主要用例:

  • 整个 Web 应用都基于 Rust 开发:比如 Yew 等框架

  • 在现存的 JavaScript 前端中使用 Rust

Yew

Github(Github 20k star):https://github.com/yewstack/yew

Yew 是一个设计先进的 Rust 框架,目的是使用 WebAssembly 来创建多线程的前端应用,它有几个特点:

  • 基于组件的框架,可以轻松地创建交互式 UI。拥有 ReactElm 等框架经验开发人员在使用 Yew 时会感到得心应手。
  • 高性能 ,前端开发者可以轻易地将工作分流至后端来减少 DOM API 的调用,从而达到异常出色的性能。(又是一个“文艺复兴式创新”...😂)
  • 支持与 JavaScript 交互 ,允许开发者使用 npm 包,并与现有的 JavaScript 应用程序结合。

一个简单的 Yew 应用代码如下所示:

use yew::prelude::*;

enum Msg {
    AddOne,
}

struct Model {
    value: i64,
}

impl Component for Model {
    type Message = Msg;
    type Properties = ();

    fn create(_ctx: &Context<Self>) -> Self {
        Self {
            value: 0,
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        match msg {
            Msg::AddOne => {
                self.value += 1;
                // the value has changed so we need to
                // re-render for it to appear on the page
                true
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        // This gives us a component's "`Scope`" which allows us to send messages, etc to the component.
        let link = ctx.link();
        html! {
            <div>
                <button onclick={link.callback(|_| Msg::AddOne)}>{ "+1" }</button>
                <p>{ self.value }</p>
            </div>
        }
    }
}

fn main() {
    yew::start_app::<Model>();
}

wasm-bindgen

Github(5.0k star):https://github.com/rustwasm/wasm-bindgen

目前 WebAssembly 类型系统还很小,只有四种数字类型,如果要使用复杂类型(例如字符串、对象、数组、结构体),需要花点心思:

  • 将字符串或对象转换为 WebAssembly 模块可以理解的东西
  • 将 WebAssembly 模块的返回值转换为 JavaScript 可以理解的字符串或对象

但是每次转换它们(序列化为线性内存,并提供它们所在位置的引用)是一项枯燥的工作并且容易出错,幸运的是,Rust world 想出了 wasm-bindgen 来促进 WebAssembly 模块和 JavaScript 之间的高级交互,其使用方式也非常简单:

  1. 创建一个 Rust 项目

    $ cargo new --lib hello_world
    Created library `hello_world` package
    
  2. 打开 Cargo.toml文件并添加 wasm-bindgen依赖项

    [package]
    name = "hello_world"
    version = "0.1.0"
    authors = ["Sendil Kumar <sendilkumarn@live.com>"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = "0.2.56"
    
  3. 打开src/lib.rs文件并将内容替换为以下内容

    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    pub fn hello_world() -> String {
      "Hello World".to_string()
    }
    
  4. 编译成 wasm 模块

    cargo build --target=wasm32-unknown-unknown
    
  5. 安装 wasm-bindgen-cli,并将 wasm 文件转换成 JavaScript 文件

      cargo install wasm-bindgen-cli
      wasm-bindgen target/wasm32-unknown-unknown/debug/hello_world.wasm --out-dir .
    # ls -lrta
    # 76330 hello_world_bg.wasm
    # 1218 hello_world.js
    #  109 hello_world.d.ts
    #  190 hello_world_bg.d.ts
    
  6. 然后你就可以使用 hello_world.js 文件了,它可以帮你加载 wasm 文件。

wasm-pack

Github(4.1k star):https://github.com/rustwasm/wasm-pack

这是一个可以直接将你的 Rust 代码打包成 npm 包的工具,用法十分简单,只有 4 个命令:

  • new:使用模板生成一个新的 Rust Wasm 项目
  • build: 从 rustwasm crate 生成一个 npm wasm pkg
  • test:运行浏览器测试
  • packpublish:创建压缩包,发布到镜像仓库

wasm-pack

值得注意的是,WebAssembly 目前还并不是提高 Web 应用性能的万金油,就目前来说,在 WebAssembly 中使用 DOM API 仍然比从 JavaScript 中调用要慢。但只是暂时性问题的,WebAssembly Interface Types 计划将解决这个问题。如果你想要了解更多关于这方面的信息,可以查看 Mozilla 的这篇文章

Rust 和 Node 的绑定

NAPI-RS

Github(2.2k star):https://github.com/napi-rs/napi-rs

NAPI-RS 是一个用于在 Rust 中构建预编译的 Node.js 插件的框架,SWC 便基于此库。

详细信息可以看《用 Rust 和 N-API 开发高性能 Node.js 扩展》这篇文章。

写在最后

随着前端开发复杂度的不断上升,配套工具链的效率将不容被忽视,受限于 Node.js 语言本身的效率问题,近几年将会有更多工具会被 Rust 重写,效率有望数倍乃至数十倍的提升,另外,Rust 和 WebAssembly 的结合也令人感到激动,但就目前来看离大规模上生产还有相当一段路需要走。

参考资料: