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


    今天基本上完成了一个操作闭环,即能够保存,拉取和删除数据。截个图
    在这里插入图片描述
    这个过程的前端和后端都是用Rust写的,前端使用的是Yew。

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

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

    在Yew的官网上,你会看到下面的介绍,我直接截图如下:
    在这里插入图片描述
    都是些溢美之词,只有体验过才知道其中的滋味。

    下面我挑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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    其中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>
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这段代码逻辑简单,我用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>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    对比一下代码,因为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>
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    编译器报错如下:

    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`
       |
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这个错误我看了好几遍,我在row上调用了clone,为什么还报所有权的错误呢?因为当进入move这个闭包函数时,所有权就发生了转移。造成这个错误的原因还是我对所有权认识不足。因此要解决这个问题,只需在进入闭包之前,先要克隆一份row的数据。那为什么在emit的调用里还要clone一次呢,那是因为外层的Callback函数里面也会发生所有权转移。所以,最后在Yew的代码中,我们会发现clone满天飞的景象。

    好了,到目前为止,在和Reactjs的对比中Yew处于劣势,但是有一点是毋庸置疑的,那就是类型安全使代码编译通过就直接跑成功。所谓难者不会,会者不难,我会继续把这条路走下去,探索Rust和Yew的优势所在,希望能够得到大家的支持和鼓励。

  • 相关阅读:
    实用指南:如何解决企业组网中网络卡顿问题?
    TCP三次握手及其相关问题
    TMS320F28374S之ADC一
    多平台商品采集——API接口:支持淘宝、天猫、1688、拼多多等多个电商平台的爆款、销量、整店商品采集和淘客功能
    ViT总结
    MFC嵌入Qt窗口详细指导
    现有版本的QT中可用数据库插件驱动查看
    【gzoj1081】k上升段【DP】
    leetcode.62. 不同路径
    第十六章:构建n(5,7)阶素数幻方
  • 原文地址:https://blog.csdn.net/firefox1/article/details/133092823