[2023.09.20]:Yew的前端开发经历小结

今天基本上完成了一个操作闭环,即能够保存,拉取和删除数据。截个图
[2023.09.20]:Yew的前端开发经历小结_第1张图片
这个过程的前端和后端都是用Rust写的,前端使用的是Yew。

Yew是一种用于构建现代Web应用程序的Rust框架,其计目标是提供一种安全、高效、易用的方式来构建Web应用程序。Yew基于WebAssembly(Wasm)技术,将Rust代码编译为能在浏览器中运行的Wasm二进制文件。这使得Yew能够充分利用Rust的内存安全和并发性能优势,同时保持前端开发的灵活性和易用性。最后,Yew采用了类似于React的组件化开发模式,这一点使其能容易被接受,毕竟Reactjs的开发人员很多,其中也包括我在内。

我追求的目标是开发一个优秀的笔记系统,我希望这个系统能够完美满足我在笔记整理方面的需求。我选择使用Yew进行前端开发,主要是因为我对Rust语言的能力有信心,并且Yew采用了Reactjs的组件化开发模式。此外,我之前有多年的Reactjs开发经验,因此最终我选择Yew。

在Yew的官网上,你会看到下面的介绍,我直接截图如下:
[2023.09.20]:Yew的前端开发经历小结_第2张图片
都是些溢美之词,只有体验过才知道其中的滋味。

下面我挑2个我体验深刻的地方来和大家分享。

1. 目录结构

首先说一下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.rsssr_server.rs分别在trunk build index.htmlcargo 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可能根本不是它们的关注点。

2. 事件处理

在前端开发中,事件处理就像我们呼吸的空气一样,随处可见而又不自知,以至于我们忘记了它的实现原理基于闭包。然而,当涉及到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的优势所在,希望能够得到大家的支持和鼓励。

你可能感兴趣的:(Rust,开发每日一篇,Javascript,rust,开发语言,前端,javascript,程序人生)