今天基本上完成了一个操作闭环,即能够保存,拉取和删除数据。截个图
这个过程的前端和后端都是用Rust写的,前端使用的是Yew。
Yew是一种用于构建现代Web应用程序的Rust框架,其计目标是提供一种安全、高效、易用的方式来构建Web应用程序。Yew基于WebAssembly(Wasm)技术,将Rust代码编译为能在浏览器中运行的Wasm二进制文件。这使得Yew能够充分利用Rust的内存安全和并发性能优势,同时保持前端开发的灵活性和易用性。最后,Yew采用了类似于React的组件化开发模式,这一点使其能容易被接受,毕竟Reactjs的开发人员很多,其中也包括我在内。
我追求的目标是开发一个优秀的笔记系统,我希望这个系统能够完美满足我在笔记整理方面的需求。我选择使用Yew进行前端开发,主要是因为我对Rust语言的能力有信心,并且Yew采用了Reactjs的组件化开发模式。此外,我之前有多年的Reactjs开发经验,因此最终我选择Yew。
在Yew的官网上,你会看到下面的介绍,我直接截图如下:
都是些溢美之词,只有体验过才知道其中的滋味。
下面我挑2个我体验深刻的地方来和大家分享。
首先说一下SSR项目的目录结构,这个例子来至于我现在正在开发的项目:
.
├── Cargo.lock
├── Cargo.toml
├── index.html
├── index.scss
├── serve.sh
└── src
├── bin
│ ├── ssr_hydrate.rs
│ └── ssr_server.rs
├── components
│ ├── base
│ │ ├── button.rs
│ │ ├── mod.rs
│ │ ├── modal.rs
│ ├── editor.rs
│ ├── mod.rs
│ └── table_component.rs
├── lib.rs
└── models.rs
其中bin
目录是Rust的规范,即用于存放可执行文件的入口。它对应的是Cargo.toml
中的[[bin]]
,这一点我在之前Yew的SSR中的Cargo.toml配置中有讲解。
这个bin
目录是SSR相关的。Yew的SSR讲得并不全面,给的例子也比较简单,幸好在它的examples文件夹中有simple_ssr和ssr_router这两个项目例子,不然都不知道该如何开始。
ssr_hydrate.rs
和ssr_server.rs
分别在trunk build index.html
和cargo run --bin ssr_server --features=ssr -- --dir dist
这两个命令中执行。前者的作用是生成dist/index.html
,后者的作用是启动服务以响应浏览器的访问。这两个文件我都是直接从simple_ssr
那边拷贝过来的,然后在ssr_server.rs
中添加反向代理的功能。
lib.rs
是组件的入口。组件的加载都从这个文件开始。因为在Rust语言中,类型的使用会很频繁(参考已经感受到了Rust类型的一等公民地位),所以我定义了一个models.rs
模块,用于存放类型和类型转换相关的代码。和Reactjs的工程一样,我也定义了一个components
文件夹用于存放各种组件。
这里吐槽一下反向代理,在Yew的SSR的文档中完全没有提及这件事情,这个功能在现阶段的主流支持SSR的Javascript框架中都是有的。幸运的是,在ssr_server.rs
中引入的warp
本身具备处理反向代理的能力。经过一系列的调研和摸索,最终还是实现了Yew的SSR模式中的反向代理功能。大家有兴趣可以参考Yew的SSR的反向代理代码编写。
关于这个目录结构,我体验深刻的就是它的这个bin
目录。这个目录其实和我们开发的组件关系不大,里面的文件几乎不用再去维护。但是没有它,SSR的功能就跑不起来。也许做Rust开发的程序员都很强,随便可以写个服务器,因此这个bin
可能根本不是它们的关注点。
在前端开发中,事件处理就像我们呼吸的空气一样,随处可见而又不自知,以至于我们忘记了它的实现原理基于闭包。然而,当涉及到Rust语言时,闭包的闭包的使用变得尤为严肃。这是因为Rust语言强调所有权的概念,闭包中使用外部数据就意味着所有权的转移,这一点需要特别注意。理解透了,在写事件代码时就会得心应手,否则,可能就会想我最开始那样,一个事件处理半天都写不出来。
下面这段Rust代码是在一个表格组件中,每一行会有一个删除按钮,点击删除按钮时触发事件,父组件负责处理这个事件。
#[function_component]
pub fn TableComponent(props: &Props) -> Html {
let Props { on_delete, .. } = props;
html! {
<div class="table_component">
<div class="table-row table-head">
<div class="td-id cell">{"id"}</div>
<div class="td-name cell">{"名称"}</div>
<div class="td-create-date cell">{"创建日期"}</div>
<div class="td-modify-date cell">{"修改日期"}</div>
<div class="td-modify-date cell">{"操作"}</div>
</div>
{
for props.data.iter().map(|row: &TableRow|{
let on_delete = on_delete.clone();
let row1 = (*row).clone();
html!{
<div class="table-row">
<div class="td-id cell">{row.id.clone()}</div>
<div class="td-name cell">{row.name.clone()}</div>
<div class="td-create-date cell">{row.created_date.to_string()}</div>
<div class="td-modify-date cell">{row.modified_date.to_string()}</div>
<div class="td-modify-date cell"><Button text="删除" display_type={DisplayType::Danger} onclick={ move |_| on_delete.emit(row1.clone()) } /></div>
</div>
}
})
}
</div>
}
}
这段代码逻辑简单,我用Reactjs来重新写一遍。
import { partial } from 'lodash';
function TableComponent (props) {
const { on_delete, data } = props;
return (
<div class="table_component">
<div class="table-row table-head">
<div class="td-id cell">{"id"}</div>
...
<div class="td-modify-date cell">{"操作"}</div>
</div>
{
data.map(row =>(
<div class="table-row">
<div class="td-id cell">{row.id}</div>
...
<div class="td-modify-date cell"><Button1 text="删除" display_type="danger" onclick={partial(on_delete, row)} /></div>
</div>
)
)
}
</div>
)
}
对比一下代码,因为Javascript中变量没有所有权的约束,所以闭包的使用很简单。在Rust这边,因为有所有权的约束,大家可以看到有很多clone()
。举一个困扰了我不少时间的例子,在上面的代码中,我去掉let row1 = (*row).clone()
,代码如下,编译器包的错就有点让我摸不着头脑。
#[function_component]
pub fn TableComponent(props: &Props) -> Html {
let Props { on_delete, .. } = props;
html! {
<div class="table_component">
...
{
for props.data.iter().map(|row: &TableRow|{
let on_delete = on_delete.clone();
// let row1 = (*row).clone();
html!{
<div class="table-row">
<div class="td-id cell">{row.id.clone()}</div>
<div class="td-name cell">{row.name.clone()}</div>
<div class="td-create-date cell">{row.created_date.to_string()}</div>
<div class="td-modify-date cell">{row.modified_date.to_string()}</div>
<div class="td-modify-date cell"><Button text="删除" display_type={DisplayType::Danger} onclick={ move |_| on_delete.emit(row.clone()) } /></div>
</div>
}
})
}
</div>
}
}
编译器报错如下:
error[E0521]: borrowed data escapes outside of function
--> src/components/table_component.rs:35:63
|
15 | pub fn TableComponent(props: &Props) -> Html {
| ----- - let's call the lifetime of this reference `'1`
| |
| `props` is a reference that is only valid in the function body
...
35 | <div class="td-modify-date cell"><Button text="删除" display_type={DisplayType::Danger} onclick={ move |_| on_delete.emit(row.clone()) } /></div>
| ^^^^^^
| |
| `props` escapes the function body here
| argument requires that `'1` must outlive `'static`
|
这个错误我看了好几遍,我在row上调用了clone,为什么还报所有权的错误呢?因为当进入move这个闭包函数时,所有权就发生了转移。造成这个错误的原因还是我对所有权认识不足。因此要解决这个问题,只需在进入闭包之前,先要克隆一份row的数据。那为什么在emit的调用里还要clone一次呢,那是因为外层的Callback函数里面也会发生所有权转移。所以,最后在Yew的代码中,我们会发现clone满天飞的景象。
好了,到目前为止,在和Reactjs的对比中Yew处于劣势,但是有一点是毋庸置疑的,那就是类型安全使代码编译通过就直接跑成功。所谓难者不会,会者不难,我会继续把这条路走下去,探索Rust和Yew的优势所在,希望能够得到大家的支持和鼓励。