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

2023-09-20 22:29:41

今天基本上完成了一个操作闭环,即能够保存,拉取和删除数据。截个图
在这里插入图片描述
这个过程的前端和后端都是用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

其中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的优势所在,希望能够得到大家的支持和鼓励。

更多推荐

【Java 基础篇】Executors工厂类详解

在多线程编程中,线程池是一项重要的工具,它可以有效地管理和控制线程的生命周期,提高程序的性能和可维护性。Java提供了java.util.concurrent包来支持线程池的创建和管理,而Executors工厂类是其中的一部分,它提供了一些方便的方法来创建不同类型的线程池。本文将详细介绍Executors工厂类的使用方

Android 使用Camera1实现相机预览、拍照、录像

1.前言本文介绍如何从零开始,在Android中实现Camera1的接入,并在文末提供Camera1Manager工具类,可以用于快速接入Camera1。AndroidCamera1API虽然已经被Google废弃,但有些场景下不得不使用。并且Camera1返回的帧数据是NV21,不像Camera2、CameraX那样

【C++】C 语言 和 C++ 语言中 const 关键字分析 ② ( const 常量分配内存时机 | const 常量在编译阶段分配内存 )

文章目录一、const常量内存分配时机二、使用如下代码验证const常量内存分配时机三、分析验证结果-const常量在编译阶段分配内存一、const常量内存分配时机在上一篇博客中,讲到了获取const常量的地址,代码如下://定义常量//该常量定义在了符号表中//符号表不在内存四区中,是另外一种机制constinta=

Pytorch 深度学习实践 day01(背景)

准备线性代数,概率论与数理统计,Python理解随机变量和分布之间的关系人类智能和人工智能人类智能分为推理和预测推理:通过外界信息的输入,来进行的推测预测:例如,看到一个真实世界的实体,把它和抽象概念联系起来人工智能(机器学习):把以前我们用来做推理或预测的大脑,变成算法在机器学习和深度学习中,常用的是监督学习,即有标

【深度学习实验】线性模型(三):使用Pytorch实现简单线性模型:搭建、构造损失函数、计算损失值

目录一、实验介绍二、实验环境1.配置虚拟环境2.库版本介绍三、实验内容0.导入库1.定义线性模型linear_model2.定义损失函数loss_function3.定义数据4.调用模型5.完整代码一、实验介绍使用Pytorch实现线性模型搭建构造损失函数计算损失值二、实验环境本系列实验使用了PyTorch深度学习框架

针对敏感数据的安全转录服务

即便在新冠肺炎疫情期间,继续保持了最高级别的机密性新冠肺炎疫情带来的各种限制向所有服务提供商提出了挑战,促使提供商们想方设法采取更富想象力的新方法来满足客户的需求。澳鹏采用了一种由两种方案组成的工作机制,服务于客户机密材料的转录,既实现了高度的机密性,又保护了员工的安全。大多数转录服务提供商都会采用基本的安全措施,如员

前端深入理解JavaScript中的WeakMap和WeakSet

🎬岸边的风:个人主页🔥个人专栏:《VUE》《javaScript》⛺️生活的理想,就是为了理想的生活!目录1.WeakMap和WeakSet概述1.1WeakMap1.2WeakSet2.WeakMap深入解析2.1WeakMap的创建和使用2.2WeakMap和内存管理2.3WeakMap和对象私有数据3.Wea

【Linux】Linux环境配置安装

目录一、双系统(特别不推荐)安装双系统的缺点:安装双系统优点(仅限老手):二、虚拟机+centos7镜像(较为推荐推荐)虚拟机的优点:虚拟机的缺点:​下载centos7的镜像文件下载Ubuntu镜像文件Ubuntu镜像文件下载地址三、云服务器Xshell云服务器共享Xshell删除用户四、powershell一、双系统

前端:运用HTML+CSS+JavaScript实现拼图游戏

前一段时间突然来了一个想法,就是运用前端知识实现一个拼图游戏,但是不知道具体怎样实现。今天,想到既然实现不了现实中我们看到的那种拼块,那么就用正方形来代替吧!效果如下:想到就是当小的图片块放到合适的位置上时,表示拼图完成。文章目录1.前端布局2.js脚本实现小图片块变换位置1.确定随机小图片块的选择2.打乱随机小图片块

阿里云无影云电脑介绍_云办公_使用_价格和优势说明

什么是阿里云无影云电脑?无影云电脑(原云桌面)是一种快速构建、高效管理桌面办公环境,无影云电脑可用于远程办公、多分支机构、安全OA、短期使用、专业制图等使用场景,阿里云百科分享无影云桌面的详细介绍、租用价格、云电脑的优势、使用场景、网络架构、无影云电脑与云服务器的区别以及关于无影云电脑的常见问题解答FAQ:目录阿里云无

以太网ARP测试实验

1.1ARP测试整体框架当上位机发送ARP请求时,FPGA返回ARP应答数据;当按下FPGA的触摸按键时,FPGA发送ARP请求,上位机返回ARP应答数据。PLL时钟对eth_rxc的输入时钟进行相位调整;GMIITORGMI模块负责将双沿(DDR)数据和单沿(SDR)数据之间的转换;ARP顶层模块实现了以太网ARP数

热文推荐