Leptos

Leptos 是用 Rust 开发的全栈 Web 框架,官网 Leptos。 我自己用他编写了一个玩具网页应用,还挺带感的:error_Q-A

file

风格

如果你熟悉 React 的写法的话,那么对 Leptos 的写法应该会很熟悉,

file

就像 React 的 function 组件,返回一串 HTML 写法的代码,不过在 Leptos 里,组件是用 view! 宏来实现的。Leptos 组件是用 Rust 写的,目前为止我没有用到过 JS 代码,在上面的应用中,逻辑代码用 Rust,界面用 HTML、CSS,借助 Tailwindcss,能帮我方便的编写一些复杂的样式效果。

构建和打包工具使用的是 Trunk,能够构建开发服务器,方便我们开始时候热重载,同时打包成 WASM 包,在浏览器中运行 Rust 代码。

与 React 不同的是,Leptos 没有采用和 React 类似的渲染机制,Leptos 没有虚拟 DOM,而是采用**最小粒度更新(fine-grained)**的方式,意味着它不需要处理虚拟 DOM,在性能上要比 React 高效。

需要一定程度上掌握 Rust 语言,Rust 本身上手门槛就高,而且 Leptos 的写法虽然与 React 类似,但是还是有很大的不同。从目前流行的前端框架转而使用 Leptos 开发前端,就等于重新学习一门高难度的语言和框架。所以,尽管 Leptos 各方面都不错,在我看来比 React 好得多,但是不可能成为流行的框架。

与 React 写法的差异

在 React 里如果需要更新页面,需要使用到 useState 这个 hook,对比虚拟 DOM 后重新渲染:

const [value, setValue] = useState(0);

// ...
setValue(value + 1);
// ...

在 Leptos 中也有类似的写法,使用的是 create_signal

let (value, set_value) = create_signal(false);

// ...
set_value.update(|v| *v = *v + 1);
//...

与 React 不同的是 create_signal最小粒度更新(fine-graint),只会更新需要更新到的地方,即便一个组件包含这大量的元素,但是它不会重新渲染整个组件,而是只修改其中有使用到 signal 的地方。为了达到这个效果,需要使用到闭包(closure)。

使用 create_signal 创建一个 signal 后,使用闭包的写法在 HTML 代码里使用这个 signal 的值:

const [value, setValue] = useState(0);

view! {
	<div>
	{
		move || value.get()
	}
	</div>
}

上面的代码会在 div 元素里渲染出 value 的值。写到一个闭包里是为了调用 set_value 修改值后能够修改页面,如果不使用闭包,

view! {
	<div>
	{value.get()}
	</div>
}

那么在调用 set_value 后页面也不会有变化。

但其实只要渲染的是一个方法就可以,所以你也可以将 signal 包装到一个方法里,然后在 view 里调用:

const [value, setValue] = useState(0);

let double = move || {
	value.get() * 2
};

view! {
	<div>
	{
		double
	}
	</div>
}

Leptos 里会大量用到类似的手法。

Rust 的写法

由于 Rust 严苛的语法规则,很容易陷入到与编译器斗智斗勇的困境中。以往写 Javascript 的随意劲头在 Rust 里是行不通的,比方说在上一节中如果有多个事件要使用到同一个变量的话,需要用到 Rc。这里涉及到 Rust 的所有权规则,比方说有这些代码:

let captcha_input: NodeRef<Input> = create_node_ref();

let handle_one = move |_| {};

let handle_two = move |_| {};

view! {
	<input node_ref=captcha_input />
	<button on:click=handle_one>BUTTON 1</button>
	<button on:click=handle_two>BUTTON 2</button>
}

如果两个按钮事件都需要获取 input 的值,那么在方法 handle_onehandle_two 里都需要使用到 captcha_input,但是由于 Rust 所有全的规则,如果在 handle_one 方法里使用到到 captcha_input 会将 captcha_input 的所有权转移到 handle_one 里, handle_two 将无法使用到 captcha_input

或许可以在方法里使用 captcha_input 的借用?也不行,因为生命周期的检查不通过,编译器无法确定这两个方法对 captcha_input 的借用到什么时候结束才是安全的。

所以要使用 Rc引用计数来持有同一个对象:

let captcha_input: NodeRef<Input> = create_node_ref();
let input_1 = Rc::new(captcha_input);
let input_2 = Rc::clone(&input_1);

let handle_one = move |_| {
	_ = input_1.get().unwrap();
};

let handle_two = move |_| {
	_ = input_2.get().unwrap();
};

view! {
	<input node_ref=captcha_input />
	<button on:click=handle_one>BUTTON 1</button>
	<button on:click=handle_two>BUTTON 2</button>
}

其实这就是纯 Rust 的写法了,需要时刻注意编译器的抱怨。

crate

Leptos 的代码最终是编译到浏览器的 WASM 环境中运行,所以不是所有的 Rust crate 都能够正常运行,像 request crate 不能在 WASM 中运行,必须挑选能够在 WASM 中使用的 crate,比如发出 http 请求的 reqwest

也有一些要模拟 Javascript API 的 crate,比如要在浏览器中使用 timeout,crate gloo 提供了能够在 WASM 中使用类似 BOM API 的功能,如果要使用 timeout 的话,需要使用 gloo

// Cargo.toml
gloo = { version = "0.11.0", features = ["timers", "futures"] }

use gloo::timers::future::TimeoutFuture;

pub async fn get_post_list() {
    TimeoutFuture::new(1_000).await;
}

Conclusion

后面或许会继续写一些 Leptos 的教程