Rust 能否成为JavaScript有力竞争者?


如果您使用 Rust 进行 Web 开发已有一段时间,您很可能会知道将 Rust 前端 Web 开发(通过 WASM)与 JavaScript 进行堆叠是一个备受争议的话题——也就是说,因为有很多人认为它“不适合生产”或“比 JavaScript 慢”。

这在过去可能是正确的:从历史上看,由于 WASM 无法访问 DOM,JavaScript 调用 WASM 会产生一些开销。

但是,目前这影响很小;基准数据显示,像 Leptos 和 Dioxus 这样的 Rust WASM 框架(它在引擎盖下使用 Sledgehammer,一个 Javascript 框架,是第三快的框架!)领先于大多数 JavaScript 框架,如 React 和 Vue。

让我们看看来自 TechEmpower 的后端基准测试:
前 10 大后端框架中有 5 个是用 Rust 编写的。很明显,Rust 作为后端框架的强大功能是不言自明的,可以轻松与 C++ 竞争。

有人可能会说 Rust 对于后端服务来说太过分了;但是,在您获得更高性能的地方,您还可以通过不需要太多内存占用以及更长的服务正常运行时间和更少的崩溃来节省资金。这是一个不应被低估的因素,因为从企业的角度来看,尽可能节省成本从来都不是一个坏主意。

尽管结果如此,但当然应该提到的是,在我们选择新框架时,速度和一般性能并不是影响我们整体决策的唯一因素。开发者体验如何?错误处理?我们如何解决 SSR 之类的问题?如果我们想对我们想要使用的东西做出最终裁决,那么这些都是需要回答的重要问题,幸运的是,Rust 有答案。

开发者体验
不管很多人怎么想,在涉及到 Web 开发时,现在 Rust 是相对宽容的。许多代码有点类似于 React 等 Web 框架的 JavaScript 组件样式 - 让我们快速深入了解 Leptos(一个 Rust Web 框架)的一些组件代码:

use leptos::*;

#[component]
pub fn SimpleCounter(cx: Scope, initial_value: i32) -> impl IntoView {
    // create a reactive signal with the initial value
    let (value, set_value) = create_signal(cx, initial_value);

   
// create event handlers for our buttons
   
// note that `value` and `set_value` are `Copy`, so it's super easy to move them into closures (for reference: closures are like anonymous/arrow functions in Javascript)
    let clear = move |_| set_value(0);
    let decrement = move |_| set_value.update(|value| *value -= 1);
    let increment = move |_| set_value.update(|value| *value += 1);

   
// create user interfaces with the declarative `view!` macro
    view! {
        cx,
        <div>
            <button on:click=clear>
"Clear"</button>
            <button on:click=decrement>
"-1"</button>
            <span>
"Value: " {value} "!"</span>
            <button on:click=increment>
"+1"</button>
        </div>
    }
}

// Easy to use with Trunk (trunkrs.dev) or with a simple wasm-bindgen setup
pub fn main() {
    mount_to_body(|cx| view! { cx,  <SimpleCounter initial_value=3 /> })
}

如您所见,代码实际上与 JSX 之类的代码相差无几——主要区别在于该组件不返回任何内容,而是使用 Rust 宏来呈现 HTML

main函数的作用类似于根文件index.js的脚本

另一个例子,来自 Dioxus:

// An example of a navbar
fn navbar(cx: Scope) -> Element {
    cx.render(rsx! {
        ul {
           
// NEW
            Link { to:
"/", "Home"}
            br {}
            Link { to:
"/blog", "Blog"}
        }
    })
}

// An example of using URL parameters
fn get_blog_post(id: &str) -> String {
    match id {
       
"foo" => "Welcome to the foo blog post!".to_string(),
       
"bar" => "This is the bar blog post!".to_string(),
        id => format!(
"Blog post '{id}' does not exist!")
    }
}


如您所见——编写 RSX(Dioxus 对 React 的 JSX 的回答)非常简单,甚至可能比使用 Leptos 更容易。您可能会发现非常明显的是,React 组件哲学超越了任何一种特定的编程语言,实际上在 Rust 中找到了一个替代。

您甚至可以将这些函数与单元结构结合起来,为您的各种函数获取命名空间,这样您就可以捆绑诸如 API 调用之类的东西,例如:

// this is pseudocode and is just to showcase bundling functions together in a unit struct, which is a struct that doesn't hold any data
pub struct APICalls;

// we make an implementation of our struct and insert the functions we want to use in it - then we can just import the struct and it'll import all of the associated methods.
Impl APICalls {
         pub async fn get_dog_api_data() -> Json<Dog> {
... some code here
// this should probably return some json data
}
         pub async fn get_cat_api_data() -> Json<Cat> {
... some code here
// this should probably return some json data
}
}

fn navbar (cx: Scope) -> Element {
// now we can call the data like this, or something similar
    let dogs = APICalls::get_dog_api_data().await;
}


真正让人眼前一亮的是 Rust 的错误处理,这是优于 JavaScript 甚至 TypeScript 的一大优势。

通常,如果您使用 Typescript 进行编码,您有两个选择:类型检查和 try-catch 块。然而,对于编码了一段时间的人来说,将代码不断地包装在 try-catch 块中并不是那么理想,TypeScript 仍然可以编译为 JavaScript,所以如果你不小心,你仍然必须处理与 JavaScript 相关的问题

让我们看一下一些基本的 Rust 处理:

async fn foo() -> Result<String, String>{
let bar = String::from("foobar!");
// due to Rust being an expressive language, return is not explicitly required here - we can just write the variable
match bar.trim() {
   
"foobar!" => Ok(bar),
      _ => Err(
"Was not foobar!".to_string())
             }
   }

#[tokio::main]
fn main() -> Result<String, String> {
let Ok(res) = foo().await else {
   return Err(
"Was not foobar :(".to_string());
}

println!(
"The string was: {res}!");
}


正如你所看到的,这个例子展示了两个例子:我们可以使用基本的模式匹配来确定字符串是什么,如果结果匹配,就返回一个OK,如果是其他东西(因此有下划线),我们可以直接返回一个具有String类型的错误(这也实现了std::error::Error--因此我们可以把它作为一个错误类型)。

我们也可以通过写(基本上)声明一个变量,声明的变量必须是一个实际的结果类型,否则就做其他事情(这里我们只是用它来提前返回)。然后我们可以使用res本身,因为它将被声明为结果中包含的值。


生态系统
虽然 Javascript 中的生态系统(通过 Node/npm)比 Rust 大得多,但这并不意味着 Rust 不能满足大多数项目的需求。Rust 目前对数据库、Redis 和许多你想包含在任何有能力的网络应用程序中的服务有很好的支持,不管你试图用什么语言编写它。

部署
在部署方面,Shuttle是迄今为止使用 Rust 开始部署的最简单方法。当涉及到后端部署时,通常情况下您可能需要弄乱配置文件或通过网站上的 GUI 添加环境变量以用于您尝试使用的服务,或者您可能难以尝试提供静态服务文件。