手记

Rust 全栈 web 应用!WASM + YEW + ROCKET

到本教程结束时,您将了解如何使用以下技术创建一个简单而完整的全栈应用程序:

对于前端:

  • Rust - 核心编程语言
  • WebAssembly - 用于在浏览器中运行 Rust
  • Yew - 用于构建客户端 web 应用的 Rust 框架
  • Trunk - 用于提供前端应用
  • Tailwind CSS - 用于前端样式

对于后端:

  • Rust - 核心编程语言
  • Rocket - 用于构建 web 服务器的 Rust 框架

对于数据库:

  • Postgres - 关系型数据库
  • Docker - 使用 Dockerfile 和 Docker Compose 运行 Postgres

哇,这么多技术!但我们尽量保持示例尽可能简单,帮助你理解核心概念。让我们开始吧!

我们将采用自下而上的方法,从数据库开始,然后是后端,最后是前端。

如果你更喜欢视频教程,可以在这里观看。

所有代码都在 GitHub (视频描述中有链接) 上可用。

架构

在开始之前,这里有一个我们将要构建的应用程序的简单架构图:

前端将使用 Yew 构建,Yew 是一个用于构建客户端 Web 应用的新 Rust 框架。Yew 受 Elm 和 React 的启发,设计简单易用。我们将使用 Trunk 来提供前端服务,并使用 Tailwind CSS 进行样式设计。所有这些都将编译为 WebAssembly 并在浏览器中运行。

后端将使用 Rocket 构建,Rocket 是一个用于 Rust 的 web 框架。Rocket 被设计为最大化开发体验。我们将使用 Rocket 构建一个简单的 REST API,该 API 将与数据库进行交互。

数据库将使用 Postgres,一个关系型数据库。我们将使用 Docker 在容器中运行 Postgres,并且不使用 ORM 以保持简单。我们将通过直接在 Rocket 处理程序中编写的 SQL 查询与数据库进行交互。

前置条件

在开始之前,请确保您的机器上已安装以下内容:

  • Rust
  • Docker

就是这样!如果你之前从未使用过WASM或Trunk,不用担心;我会展示你需要运行的命令。

准备工作。

我们将有一个包含以下子文件夹的文件夹:

  • 后端
  • 前端

所以,让我们创建一个新的文件夹,切换到该文件夹,并在任何你想要的IDE中打开它。

我将使用 Visual Studio Code。

    mkdir rustfs
    cd rustfs
    code .

进入全屏模式 退出全屏模式

从根目录初始化一个 git 仓库。

    git init

进入全屏模式 退出全屏模式

并且创建一个 compose.yml 文件(这将用于运行 Postgres 数据库)

你应该得到类似这样的结果:

我们现在准备好构建应用程序了。在下一节中,我们将设置数据库。

设置数据库

我们将使用 Docker 在容器中运行一个 Postgres 数据库。这将使您能够在不安装 Postgres 的情况下轻松地在本地运行数据库。

打开 compose.yml 文件并添加以下内容:

    services:
      db:
        container_name: db
        image: postgres:12
        ports:
          - "5432:5432"
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: postgres
        volumes:
          - pgdata:/var/lib/postgresql/data

    volumes:
      pgdata: {}

进入全屏模式 退出全屏模式

  • db 是服务的名称
  • container_name 是容器的名称,我们将使用 db
  • image 是 Postgres 镜像(我们将使用 Postgres 12)
  • ports 是端口映射(5432:5432)
  • environment 是 Postgres 实例的环境变量
  • volumes 是 Postgres 数据的卷映射

我们还定义了一个卷 pgdata,用于存储 Postgres 数据。

现在,运行以下命令来启动 Postgres 数据库:

    docker compose up

进入全屏模式 退出全屏模式

你应该在终端中看到 Postgres 日志。如果你看到 database system is ready to accept connections,数据库很可能运行成功了。

要进行另一次测试,您可以在终端中输入:

    docker ps -a

进入全屏模式 退出全屏模式

你应该看到数据库正在运行:

您也可以通过运行以下命令进入数据库容器:

    docker exec -it db psql -U postgres

进入全屏模式 退出全屏模式

你可以通过运行以下命令来查看当前的数据库:

    \dt

进入全屏模式 退出全屏模式

你应该看到以下输出(未找到任何关系):

这是因为我们还没有创建任何表。我们将在下一节中完成这个任务。

设置后端

我们将使用 Rocket 构建后端。

Rocket 是一个为 Rust 设计的 web 框架,旨在最大化开发者的体验。我们将使用 Rocket 构建一个简单的 REST API 来与数据库进行交互。

创建一个名为 backend 的新 Rust 项目,不初始化 git 仓库:

    cargo new backend --vcs none

进入全屏模式 退出全屏模式

你的项目结构应该如下所示:

打开 Cargo.toml 文件并添加以下依赖项:

    rocket = { version = "0.5", features = ["json"] }
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    tokio = { version = "1", features = ["full"] }
    tokio-postgres = "0.7.11"
    rocket_cors = { version = "0.6.0", default-features = false }

进入全屏模式 退出全屏模式

  • rocket 是我们用来构建后端的 Rocket 网络框架
  • serde 是一个序列化/反序列化库
  • serde_json 是一个 JSON 序列化/反序列化库
  • tokio 是 Rust 的异步运行时
  • tokio-postgres 是一个用于 Tokio 的 Postgres 客户端
  • rocket_cors 是一个用于 Rocket 的 CORS 库

现在,打开 /backend/main.rs 文件并将内容替换为以下内容(见下方解释):

#[macro_use]
extern crate rocket;

use rocket::serde::{Deserialize, Serialize, json::Json};
use rocket::{State, response::status::Custom, http::Status};
use tokio_postgres::{Client, NoTls};
use rocket_cors::{CorsOptions, AllowedOrigins};

#[derive(Serialize, Deserialize, Clone)]
struct User {
    id: Option<i32>,
    name: String,
    email: String,
}

#[post("/api/users", data = "<user>")]
async fn add_user(
    conn: &State<Client>,
    user: Json<User>
) -> Result<Json<Vec<User>>, Custom<String>> {
    execute_query(
        conn,
        "INSERT INTO users (name, email) VALUES ($1, $2)",
        &[&user.name, &user.email]
    ).await?;
    get_users(conn).await
}

#[get("/api/users")]
async fn get_users(conn: &State<Client>) -> Result<Json<Vec<User>>, Custom<String>> {
    get_users_from_db(conn).await.map(Json)
}

async fn get_users_from_db(client: &Client) -> Result<Vec<User>, Custom<String>> {
    let users = client
        .query("SELECT id, name, email FROM users", &[]).await
        .map_err(|e| Custom(Status::InternalServerError, e.to_string()))?
        .iter()
        .map(|row| User { id: Some(row.get(0)), name: row.get(1), email: row.get(2) })
        .collect::<Vec<User>>();

    Ok(users)
}

#[put("/api/users/<id>", data = "<user>")]
async fn update_user(
    conn: &State<Client>,
    id: i32,
    user: Json<User>
) -> Result<Json<Vec<User>>, Custom<String>> {
    execute_query(
        conn,
        "UPDATE users SET name = $1, email = $2 WHERE id = $3",
        &[&user.name, &user.email, &id]
    ).await?;
    get_users(conn).await
}

#[delete("/api/users/<id>")]
async fn delete_user(conn: &State<Client>, id: i32) -> Result<Status, Custom<String>> {
    execute_query(conn, "DELETE FROM users WHERE id = $1", &[&id]).await?;
    Ok(Status::NoContent)
}

async fn execute_query(
    client: &Client,
    query: &str,
    params: &[&(dyn tokio_postgres::types::ToSql + Sync)]
) -> Result<u64, Custom<String>> {
    client
        .execute(query, params).await
        .map_err(|e| Custom(Status::InternalServerError, e.to_string()))
}

#[launch]
async fn rocket() -> _ {
    let (client, connection) = tokio_postgres
        ::connect("host=localhost user=postgres password=postgres dbname=postgres", NoTls).await
        .expect("Failed to connect to Postgres");

    tokio::spawn(async move {
        if let Err(e) = connection.await {
            eprintln!("Failed to connect to Postgres: {}", e);
        }
    });

    // 创建表,如果表不存在
    client
        .execute(
            "CREATE TABLE IF NOT EXISTS users (
                id SERIAL PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT NOT NULL
            )",
            &[]
        ).await
        .expect("Failed to create table");

    let cors = CorsOptions::default()
        .allowed_origins(AllowedOrigins::all())
        .to_cors()
        .expect("Error while building CORS");

    rocket
        ::build()
        .manage(client)
        .mount("/", routes![add_user, get_users, update_user, delete_user])
        .attach(cors)
}

进入全屏模式 退出全屏模式

这段视频中,我解释了上面的代码。

解释

  • 我们在文件的顶部导入所有需要的内容。我们还定义了一个 macro_use 属性来导入 rocket 宏。
  • 我们定义了一个 User 结构体来表示用户数据。这个结构体会被序列化/反序列化为 JSON(注意:id 是一个 Option,因为我们不想在创建新用户时提供 id,它将由数据库分配)。
  • 我们定义了一个 add_user 路由,用于将新用户插入到数据库中。我们使用 execute_query 函数来执行 SQL 查询。然后调用 get_users 函数返回所有用户。
  • 我们定义了一个 get_users 路由,用于从数据库中返回所有用户。
  • 我们定义了一个 update_user 路由,用于更新数据库中的用户。我们使用 execute_query 函数来执行 SQL 查询。然后调用 get_users 函数返回所有用户。
  • 我们定义了一个 delete_user 路由,用于从数据库中删除用户。我们使用 execute_query 函数来执行 SQL 查询。
  • 我们定义了一个 execute_query 函数,用于在数据库上执行 SQL 查询。
  • 我们定义了一个 rocket 函数来创建 Rocket 实例。我们连接到 Postgres 数据库,并使用 SQL 查询创建 users 表(如果它不存在)。然后创建 CORS 选项并将其附加到 Rocket 实例上。即使我们在同一台机器上运行前端和后端,我们也需要启用 CORS 以允许前端向后端发送请求。

我们现在可以通过运行以下命令来启动后端:

    cargo run

进入全屏模式 退出全屏模式

我们应该看到以下输出:

你可以访问以下 URL: http://127.0.0.1:8000/api/users,你应该会看到一个空数组 []

使用 Postman 测试 API

你可以使用 Postman 测试 API。

你可以通过发送一个 GET 请求到 http://127.0.0.1:8000/api/users 来获取用户列表。

你可以通过发送一个 POST 请求到 http://127.0.0.1:8000/api/users 并携带以下 JSON 请求体来创建一个新的用户:

    {
        "姓名": "AAA",
        "邮箱": "aaa@mail.com"
    }

进入全屏模式 退出全屏模式

你可以再创建2个用户:

    {
        "name": "BBB",
        "email": "
    }

进入全屏模式 退出全屏模式

    {
        "name": "CCC",
        "email": "
    }

进入全屏模式 退出全屏模式

你应该看到以下输出:

要更新用户,你可以发送一个 PUT 请求到 http://127.0.0.1:8000/api/users/2,并附带以下 JSON 请求体:

    {
        "姓名": "Francesco",
        "邮箱": "francesco@mail"
    }

进入全屏模式 退出全屏模式

并且我们应该看到更新后的用户:

要删除一个用户,你可以发送一个 DELETE 请求到 http://127.0.0.1:8000/api/users/1

我们应该得到一个 204 响应(资源已被删除):

如果我们尝试获取所有用户,我们应该看到以下输出:

我们可以使用浏览器检查地址 http://127.0.0.1:8000/api/users 下的用户,从而验证这一点。

我们也可以直接在 Postgres 数据库中运行以下命令进行测试:
(如果你关闭了终端,可以通过运行 docker exec -it db psql -U postgres 进入容器)

    \dt
    select * from users;

进入全屏模式 退出全屏模式

恭喜!您已经成功设置了后端。在下一节中,我们将设置前端。

设置前端

现在,我们来处理前端。我们将使用 Yew 来构建它。Yew 是一个用于构建客户端 Web 应用程序的 Rust 框架。我们将使用 Trunk 来构建和打包前端,并使用 Tailwind CSS 进行样式设计。所有这些都将编译为 WebAssembly 并在浏览器中运行。

重要! 如果你从未在你的机器上使用过 Rust 的 Wasm,你可以通过运行以下命令来安装它:

    rustup target add wasm32-unknown-unknown

进入全屏模式 退出全屏模式

重要! 您还必须在您的机器上安装 trunk。可以通过运行以下命令进行安装:

    cargo install trunk

进入全屏模式 退出全屏模式

你可以通过运行以下命令来验证 trunk 是否已安装:

    trunk --version

进入全屏模式 退出全屏模式

现在你可以创建一个名为 frontend 的新 Rust 项目(确保你在 rustfs 文件夹中):

    cargo new frontend --vcs none

进入全屏模式 退出全屏模式

现在打开 frontend/Cargo.toml 文件并添加以下依赖项:

    [package]
    name = "前端"
    version = "0.1.0"
    edition = "2021"

    [dependencies]
    yew = { version = "0.21", features = ["csr"] }
    wasm-bindgen = "0.2"
    web-sys = { version = "0.3", features = ["console"] }
    gloo = "0.6"
    wasm-bindgen-futures = "0.4"  
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"

进入全屏模式 退出全屏模式

  • yew 是 Yew 框架(用于构建客户端 Web 应用的 Rust 框架)
  • wasm-bindgen 是一个库,用于促进 WebAssembly 和 JavaScript 之间的通信
  • web-sys 是一个提供 Web API 绑定的库
  • gloo 是一个提供 WebAssembly 工具的库
  • wasm-bindgen-futures 是一个提供 WebAssembly 中处理 futures 工具的库
  • serde 是一个序列化/反序列化库
  • serde_json 是一个 JSON 序列化/反序列化库

现在在 frontend 文件夹中创建一个名为 index.html 的新文件,并添加以下内容:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>Yew + Tailwind</title>
        <script src="https://cdn.tailwindcss.com"></script>
      </head>
      <body>
        <div id="app"></div>
        <script type="module">
          import init from './pkg/frontend.js';
          init();
        </script>
      </body>
    </html>

进入全屏模式 退出全屏模式

  • 在 HTML 文件的 head 部分导入 Tailwind CSS CDN
  • 创建一个 id 为 app 的 div,Yew 应用将被挂载到这里
  • 导入将由 Trunk 生成的 frontend.js 文件

现在打开 frontend/src/main.rs 文件,并将其内容替换为以下内容:

    使用 yew::prelude::*;
    使用 serde::{ Deserialize, Serialize };
    使用 gloo::net::http::Request;
    使用 wasm_bindgen_futures::spawn_local;

    #[derive(Serialize, Deserialize, Clone, Debug)]
    结构体 User {
        id: i32,
        name: String,
        email: String,
    }

    fn main() {
        yew::Renderer::<App>::new().render();
    }

进入全屏模式 退出全屏模式

  • 我们导入必要的依赖
  • 我们定义一个 User 结构体来表示用户数据
  • 我们定义 main 函数来渲染 Yew 应用

但这还不够。我们需要添加 App 组件。我们可以使用外部文件,但为了简单起见,我们将直接在 main.rs 文件中添加它。

以下是你应该添加到 main.rs 文件中的代码。

这段代码定义了一个名为 App 的 Yew 函数组件,用于管理网页应用中的用户数据和交互。use_state 钩子初始化了用于管理用户信息 (user_state)、消息 (message) 和用户列表 (users) 的状态。

该组件定义了几个回调函数,用于与后端API交互:

  • get_users: 从后端API获取用户列表并更新用户状态。如果请求失败,则设置错误消息。
  • create_user: 使用来自 user_state 的数据发送 POST 请求创建新用户。成功后,触发 get_users 回调刷新用户列表。
  • update_user: 通过发送 PUT 请求更新现有用户的资料。如果成功,则刷新用户列表并重置 user_state。
  • delete_user: 发送 DELETE 请求根据用户 ID 删除用户。成功后,刷新用户列表。
  • edit_user: 通过将所选用户的详细信息更新到 user_state 来准备用户信息以进行编辑。

这些回调利用异步操作 (spawn_local) 处理网络请求,而不阻塞UI线程,确保了流畅的用户体验。

    ...
    #[function_component(App)]
    fn app() -> Html {
        let user_state = use_state(|| ("".to_string(), "".to_string(), None as Option<i32>));
        let message = use_state(|| "".to_string());
        let users = use_state(Vec::new);

        let get_users = {
            let users = users.clone();
            let message = message.clone();
            Callback::from(move |_| {
                let users = users.clone();
                let message = message.clone();
                spawn_local(async move {
                    match Request::get("http://127.0.0.1:8000/api/users").send().await {
                        Ok(resp) if resp.ok() => {
                            let fetched_users: Vec<User> = resp.json().await.unwrap_or_default();
                            users.set(fetched_users);
                        }

                        _ => message.set("获取用户失败".into()),
                    }
                });
            })
        };

        let create_user = {
            let user_state = user_state.clone();
            let message = message.clone();
            let get_users = get_users.clone();
            Callback::from(move |_| {
                let (name, email, _) = (*user_state).clone();
                let user_state = user_state.clone();
                let message = message.clone();
                let get_users = get_users.clone();

                spawn_local(async move {
                    let user_data = serde_json::json!({ "name": name, "email": email });

                    let response = Request::post("http://127.0.0.1:8000/api/users")
                        .header("Content-Type", "application/json")
                        .body(user_data.to_string())
                        .send().await;

                    match response {
                        Ok(resp) if resp.ok() => {
                            message.set("用户创建成功".into());
                            get_users.emit(());
                        }

                        _ => message.set("用户创建失败".into()),
                    }

                    user_state.set(("".to_string(), "".to_string(), None));
                });
            })
        };

        let update_user = {
            let user_state = user_state.clone();
            let message = message.clone();
            let get_users = get_users.clone();

            Callback::from(move |_| {
                let (name, email, editing_user_id) = (*user_state).clone();
                let user_state = user_state.clone();
                let message = message.clone();
                let get_users = get_users.clone();

                if let Some(id) = editing_user_id {
                    spawn_local(async move {
                        let response = Request::put(&format!("http://127.0.0.1:8000/api/users/{}", id))
                            .header("Content-Type", "application/json")
                            .body(serde_json::to_string(&(id, name.as_str(), email.as_str())).unwrap())
                            .send().await;

                        match response {
                            Ok(resp) if resp.ok() => {
                                message.set("用户更新成功".into());
                                get_users.emit(());
                            }

                            _ => message.set("用户更新失败".into()),
                        }

                        user_state.set(("".to_string(), "".to_string(), None));
                    });
                }
            })
        };

        let delete_user = {
            let message = message.clone();
            let get_users = get_users.clone();

            Callback::from(move |id: i32| {
                let message = message.clone();
                let get_users = get_users.clone();

                spawn_local(async move {
                    let response = Request::delete(
                        &format!("http://127.0.0.1:8000/api/users/{}", id)
                    ).send().await;

                    match response {
                        Ok(resp) if resp.ok() => {
                            message.set("用户删除成功".into());
                            get_users.emit(());
                        }

                        _ => message.set("用户删除失败".into()),
                    }
                });
            })
        };

        let edit_user = {
            let user_state = user_state.clone();
            let users = users.clone();

            Callback::from(move |id: i32| {
                if let Some(user) = users.iter().find(|u| u.id == id) {
                    user_state.set((user.name.clone(), user.email.clone(), Some(id)));
                }
            })
        };
    ...

进入全屏模式 退出全屏模式

你可以逐行检查编写代码的过程,在视频的这部分

现在我们需要添加由 Yew 组件渲染的 HTML 代码。以下是你应该在 main.rs 文件中添加的代码。

如果你熟悉 React,这与 JSX 文件中发生的情况类似。

这段使用 Yew 的 html! 宏编写的 HTML 部分定义了 Yew 应用程序的用户界面。它由几个关键部分组成,提供了管理用户的功能。

  • 使用 Tailwind CSS 创建一个带有一定内边距和良好布局的主要容器。
  • 顶部有一个大标题,写着“用户管理”,让用户知道应用程序的用途。
  • 两个输入框:一个用于输入用户名,另一个用于输入用户邮箱。当你输入内容时,它会更新状态以跟踪输入的内容。
  • 一个按钮,它的操作和标签会根据你是创建新用户还是更新现有用户而变化。如果是添加新用户,按钮上会显示 创建用户,如果是编辑现有用户,则显示 更新用户
  • 输入框下方有一个消息显示区域,用于显示成功或错误消息(文本颜色始终为绿色,出现错误时可以将其变为红色)。
  • 一个 获取用户列表 按钮,点击后会从后端获取最新的用户数据。
  • 一个列出从后端获取的所有用户的区域,显示每个用户的 ID、姓名和邮箱。
  • 列表中的每个用户都有一个“删除”按钮来移除用户和一个“编辑”按钮来加载其详细信息到输入框中进行编辑。
    ...
    html! {
            <div class="container mx-auto p-4">
                <h1 class="text-4xl font-bold text-blue-500 mb-4">{ "用户管理" }</h1>
                    <div class="mb-4">
                        <input
                            placeholder="姓名"
                            value={user_state.0.clone()}
                            oninput={Callback::from({
                                let user_state = user_state.clone();
                                move |e: InputEvent| {
                                    let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
                                    user_state.set((input.value(), user_state.1.clone(), user_state.2));
                                }
                            })}
                            class="border rounded px-4 py-2 mr-2"
                        />
                        <input
                            placeholder="邮箱"
                            value={user_state.1.clone()}
                            oninput={Callback::from({
                                let user_state = user_state.clone();
                                move |e: InputEvent| {
                                    let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
                                    user_state.set((user_state.0.clone(), input.value(), user_state.2));
                                }
                            })}
                            class="border rounded px-4 py-2 mr-2"
                        />

                        <button
                            onclick={if user_state.2.is_some() { update_user.clone() } else { create_user.clone() }}
                            class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                        >
                            { if user_state.2.is_some() { "更新用户" } else { "创建用户" } }

                        </button>
                            if !message.is_empty() {
                            <p class="text-green-500 mt-2">{ &*message }</p>
                        }
                    </div>

                    <button
                        onclick={get_users.reform(|_| ())}  
                        class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"
                    >
                        { "获取用户列表" }
                    </button>

                    <h2 class="text-2xl font-bold text-gray-700 mb-2">{ "用户列表" }</h2>

                    <ul class="list-disc pl-5">
                        { for (*users).iter().map(|user| {
                            let user_id = user.id;
                            html! {
                                <li class="mb-2">
                                    <span class="font-semibold">{ format!("ID: {}, 姓名: {}, 邮箱: {}", user.id, user.name, user.email) }</span>
                                    <button
                                        onclick={delete_user.clone().reform(move |_| user_id)}
                                        class="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded"
                                    >
                                        { "删除" }
                                    </button>
                                    <button
                                        onclick={edit_user.clone().reform(move |_| user_id)}
                                        class="ml-4 bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-1 px-2 rounded"
                                    >
                                        { "编辑" }
                                    </button>
                                </li>
                            }
                        })}

                    </ul>

            </div>
        }
    ...

进入全屏模式 退出全屏模式

你可以在这段视频中逐行检查编写代码的过程

这里是 /frontend/src/main.rs 文件的完整代码:

    使用 yew::prelude::*;
    使用 serde::{ Deserialize, Serialize };
    使用 gloo::net::http::Request;
    使用 wasm_bindgen_futures::spawn_local;

    #[function_component(App)]
    fn app() -> Html {
        let user_state = use_state(|| ("".to_string(), "".to_string(), None as Option<i32>));
        let message = use_state(|| "".to_string());
        let users = use_state(Vec::new);

        let get_users = {
            let users = users.clone();
            let message = message.clone();
            Callback::from(move |_| {
                let users = users.clone();
                let message = message.clone();
                spawn_local(async move {
                    match Request::get("http://127.0.0.1:8000/api/users").send().await {
                        Ok(resp) if resp.ok() => {
                            let fetched_users: Vec<User> = resp.json().await.unwrap_or_default();
                            users.set(fetched_users);
                        }

                        _ => message.set("Failed to fetch users".into()),
                    }
                });
            })
        };

        let create_user = {
            let user_state = user_state.clone();
            let message = message.clone();
            let get_users = get_users.clone();
            Callback::from(move |_| {
                let (name, email, _) = (*user_state).clone();
                let user_state = user_state.clone();
                let message = message.clone();
                let get_users = get_users.clone();

                spawn_local(async move {
                    let user_data = serde_json::json!({ "name": name, "email": email });

                    let response = Request::post("http://127.0.0.1:8000/api/users")
                        .header("Content-Type", "application/json")
                        .body(user_data.to_string())
                        .send().await;

                    match response {
                        Ok(resp) if resp.ok() => {
                            message.set("User created successfully".into());
                            get_users.emit(());
                        }

                        _ => message.set("Failed to create user".into()),
                    }

                    user_state.set(("".to_string(), "".to_string(), None));
                });
            })
        };

        let update_user = {
            let user_state = user_state.clone();
            let message = message.clone();
            let get_users = get_users.clone();

            Callback::from(move |_| {
                let (name, email, editing_user_id) = (*user_state).clone();
                let user_state = user_state.clone();
                let message = message.clone();
                let get_users = get_users.clone();

                if let Some(id) = editing_user_id {
                    spawn_local(async move {
                        let response = Request::put(&format!("http://127.0.0.1:8000/api/users/{}", id))
                            .header("Content-Type", "application/json")
                            .body(serde_json::to_string(&(id, name.as_str(), email.as_str())).unwrap())
                            .send().await;

                        match response {
                            Ok(resp) if resp.ok() => {
                                message.set("User updated successfully".into());
                                get_users.emit(ago);
                            }

                            _ => message.set("Failed to update user".into()),
                        }

                        user_state.set(("".to_string(), "".to_string(), None));
                    });
                }
            })
        };

        let delete_user = {
            let message = message.clone();
            let get_users = get_users.clone();

            Callback::from(move |id: i32| {
                let message = message.clone();
                let get_users = get_users.clone();

                spawn_local(async move {
                    let response = Request::delete(
                        &format!("http://127.0.0.1:8000/api/users/{}", id)
                    ).send().await;

                    match response {
                        Ok(resp) if resp.ok() => {
                            message.set("User deleted successfully".into());
                            get_users.emit(());
                        }

                        _ => message.set("Failed to delete user".into()),
                    }
                });
            })
        };

        let edit_user = {
            let user_state = user_state.clone();
            let users = users.clone();

            Callback::from(move |id: i32| {
                if let Some(user) = users.iter().find(|u| u.id == id) {
                    user_state.set((user.name.clone(), user.email.clone(), Some(id)));
                }
            })
        };

        //html

        html! {
            <div class="container mx-auto p-4">
                <h1 class="text-4xl font-bold text-blue-500 mb-4">{ "用户管理" }</h1>
                    <div class="mb-4">
                        <input
                            placeholder="姓名"
                            value={user_state.0.clone()}
                            oninput={Callback::from({
                                let user_state = user_state.clone();
                                move |e: InputEvent| {
                                    let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
                                    user_state.set((input.value(), user_state.1.clone(), user_state.2));
                                }
                            })}
                            class="border rounded px-4 py-2 mr-2"
                        />
                        <input
                            placeholder="邮箱"
                            value={user_state.1.clone()}
                            oninput={Callback::from({
                                let user_state = user_state.clone();
                                move |e: InputEvent| {
                                    let input = e.target_dyn_into::<web_sys::HtmlInputElement>().unwrap();
                                    user_state.set((user_state.0.clone(), input.value(), user_state.2));
                                }
                            })}
                            class="border rounded px-4 py-2 mr-2"
                        />

                        <button
                            onclick={if user_state.2.is_some() { update_user.clone() } else { create_user.clone() }}
                            class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                        >
                            { if user_state.2.is_some() { "更新用户" } else { "创建用户" } }

                        </button>
                            if !message.is_empty() {
                            <p class="text-green-500 mt-2">{ &*message }</p>
                        }
                    </div>

                    <button
                        onclick={get_users.reform(|_| ())}  
                        class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mb-4"
                    >
                        { "获取用户列表" }
                    </button>

                    <h2 class="text-2xl font-bold text-gray-700 mb-2">{ "用户列表" }</h2>

                    <ul class="list-disc pl-5">
                        { for (*users).iter().map(|user| {
                            let user_id = user.id;
                            html! {
                                <li class="mb-2">
                                    <span class="font-semibold">{ format!("ID: {}, 姓名: {}, 邮箱: {}", user.id, user.name, user.email) }</span>
                                    <button
                                        onclick={delete_user.clone().reform(move |_| user_id)}
                                        class="ml-4 bg-red-500 hover:bg-red-700 text-white font-bold py-1 px-2 rounded"
                                    >
                                        { "删除" }
                                    </button>
                                    <button
                                        onclick={edit_user.clone().reform(move |_| user_id)}
                                        class="ml-4 bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-1 px-2 rounded"
                                    >
                                        { "编辑" }
                                    </button>
                                </li>
                            }
                        })}

                    </ul>

            </div>
        }
    }

    #[derive(Serialize, Deserialize, Clone, Debug)]
    struct User {
        id: i32,
        name: String,
        email: String,
    }

    fn main() {
        yew::Renderer::<App>::new().render();
    }

进入全屏模式 退出全屏模式

构建前端

现在是运行前端的时候了。

你可以输入:

    cargo build --target wasm32-unknown-unknown

进入全屏模式 退出全屏模式

然后你可以通过运行以下命令来运行前端:

    trunk serve

进入全屏模式 退出全屏模式

你现在可以访问 http://127.0.0.1:8080 并点击 获取用户列表 按钮从后端获取用户列表:

如你所见,我们从后端获取用户信息并在前端显示。前端还允许你创建、更新和删除用户。

例如,我们可以创建一个名为 yew 和邮箱 yes@mail.com 的用户:

用户应在前端正确显示,显示消息 用户创建成功

为了检查数据的一致性,我们可以使用 Postman,向 http://127.0.0.1:8000/api/users 发送一个 GET 请求:

我们也可以更新一个用户,例如ID为3的用户,将其名字改为subscribe,邮箱改为subscribe@mail.com。请注意,当我们点击编辑按钮时,表单会填充用户数据,并且按钮标签会变为更新用户

点击 更新用户 按钮后,我们应该看到消息 用户更新成功

最后一个测试是删除一个用户,例如ID为3的用户。点击删除按钮后,我们应该看到消息用户删除成功

点击 删除 按钮后,我们应该看到消息 用户删除成功

注意: 您应该能够在后端日志中看到所有的 HTTP 请求。

让我们创建最后一个用户,并将其命名为 last,电子邮件使用 last@mail.com

如果我们使用 Postman 并向 http://127.0.0.1:8000/api/users 发起一个 GET 请求,我们应该看到以下输出:

我们也可以通过打开一个新的标签页并访问 http://127.0.0.1:8000/api/users 来查看数据:

最后一个测试是在 Postgres 容器中直接检查。你可以通过运行 docker exec -it db psql -U postgres 进入容器,然后运行:

    \dt
    select * from users;

进入全屏模式 退出全屏模式

做得好!

结论

在本教程中,我们使用 Rust 构建了一个全栈 web 应用程序。我们使用 Rocket 和 Postgres 构建了后端,并使用 Yew、Tailwind CSS 和 Trunk 构建了前端。我们通过前端从数据库中创建、读取、更新和删除用户。我们还使用 Postman 测试了 API 并进行了检查

如果你更喜欢视频版本:

所有代码都在 GitHub (视频描述中有链接) 上可用。

你可以在这里找到我: https://francescociulla.com

0人推荐
随时随地看视频
慕课网APP