Rust fullstack web app! WASM + YEW + ROCKET

2025-06-08

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

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

对于前端:

  • Rust - 核心编程语言
  • Web Assembly - 用于在浏览器中运行 Rust
  • Yew - 用于构建客户端 Web 应用程序的 Rust 框架
  • Trunk - 用于服务前端应用程序
  • Tailwind CSS - 用于设置前端样式

对于后端:

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

对于数据库:

  • Postgres - 关系数据库
  • Docker - 用于运行 Postgres 的 Dockerfile 和 Docker Compose

哇,这么多技术!不过我们会尽量简化示例,帮助你理解核心概念。现在就开始吧!

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

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

所有代码均可在GitHub上找到(链接见视频说明)

建筑学

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

构建 Rust 全栈 Web 应用程序

前端将使用 Yew 构建,这是一个用于构建客户端 Web 应用的全新 Rust 框架。Yew 的设计灵感源自 Elm 和 React,简洁易用。我们将使用 Trunk 提供前端服务,并使用 Tailwind CSS 进行样式设置。所有这些代码都将编译为 Web Assembly 并在浏览器中运行。

后端将使用 Rocket(一个 Rust 的 Web 框架)构建。Rocket 旨在最大限度地提升开发者体验。我们将使用 Rocket 构建一个与数据库交互的简单 REST API。

数据库将是 Postgres,一个关系型数据库。我们将使用 Docker 在容器中运行 Postgres,并且为了简化操作,我们不使用 ORM。我们将使用直接在 Rocket 处理程序中编写的 SQL 查询与数据库进行交互。

先决条件

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

  • Docker

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

准备。

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

  • 后端
  • 前端

因此,让我们创建一个新文件夹,导航它,然后在您想要的任何 IDE 中打开它。

我将使用 Visual Studio Code。



mkdir rustfs
cd rustfs
code .


Enter fullscreen mode Exit fullscreen mode

从根文件夹初始化 git 存储库。



git init


Enter fullscreen mode Exit fullscreen mode

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

你应该得到如下结果:

构建 Rust 全栈 Web 应用程序

现在我们可以开始构建应用程序了。下一节我们将设置数据库。

设置数据库

我们将使用 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: {}


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

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

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



docker compose up


Enter fullscreen mode Exit fullscreen mode

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

构建 Rust 全栈 Web 应用程序

要进行另一项测试,您可以进入终端并输入:



docker ps -a


Enter fullscreen mode Exit fullscreen mode

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

构建 Rust 全栈 Web 应用程序

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



docker exec -it db psql -U postgres


Enter fullscreen mode Exit fullscreen mode

您可以通过运行以下命令检查当前数据库:



\dt


Enter fullscreen mode Exit fullscreen mode

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

构建 Rust 全栈 Web 应用程序

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

设置后端

我们将使用 Rocket 来构建后端。

Rocket 是一个 Rust 的 Web 框架,旨在最大限度地提升开发者体验。我们将使用 Rocket 构建一个与数据库交互的简单 REST API。

创建一个名为 的新 Rust 项目backend,无需初始化 git 存储库:



cargo new backend --vcs none


Enter fullscreen mode Exit fullscreen mode

您的项目结构应如下所示:

构建 Rust 全栈 Web 应用程序

打开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 }


Enter fullscreen mode Exit fullscreen mode
  • rocket是我们将用来构建后端的 Rocket Web 框架
  • 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);
        }
    });

    //Create the table if it doesn't exist
    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)
}


Enter fullscreen mode Exit fullscreen mode

视频的这一部分,我解释了上面的代码。

解释

  • 我们在文件顶部进行所有导入。我们还定义了一个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 数据库,并users使用 SQL 查询创建表(如果不存在)。然后,我们创建 CORS 选项并将其附加到 Rocket 实例。即使我们在同一台机器上运行前端和后端,也需要启用 CORS 以允许前端向后端发出请求。

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



cargo run


Enter fullscreen mode Exit fullscreen mode

我们应该看到以下输出:

构建 Rust 全栈 Web 应用程序

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

构建 Rust 全栈 Web 应用程序

使用 Postman 测试 API

您可以使用 Postman 测试 API。

GET您可以通过发送请求到以下地址来获取用户列表http://127.0.0.1:8000/api/users

构建 Rust 全栈 Web 应用程序

您可以通过发送带有以下 JSON 主体的POST请求来创建新用户:http://127.0.0.1:8000/api/users



{
    "name": "AAA",
    "email": "aaa@mail.com"
}


Enter fullscreen mode Exit fullscreen mode

构建 Rust 全栈 Web 应用程序

您可以再创建 2 个用户:



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


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

您应该看到以下输出:

构建 Rust 全栈 Web 应用程序

要更新用户,您可以发送带有以下 JSON 主体的PUT请求:http://127.0.0.1:8000/api/users/2



{
    "name": "Francesco",
    "email": "francesco@mail"
}


Enter fullscreen mode Exit fullscreen mode

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

构建 Rust 全栈 Web 应用程序

要删除用户,您可以发送DELETE请求至http://127.0.0.1:8000/api/users/1

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

构建 Rust 全栈 Web 应用程序

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

构建 Rust 全栈 Web 应用程序

如果我们使用浏览器检查该地址的用户,我们可以看到这是一致的http://127.0.0.1:8000/api/users

构建 Rust 全栈 Web 应用程序

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



\dt
select * from users;


Enter fullscreen mode Exit fullscreen mode

构建 Rust 全栈 Web 应用程序

恭喜!您已成功设置后端。下一节我们将设置前端。

设置前端

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

重要!如果您从未在计算机上使用过 Wasm for Rust,则可以通过运行以下命令进行安装:



rustup target add wasm32-unknown-unknown


Enter fullscreen mode Exit fullscreen mode

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



cargo install trunk


Enter fullscreen mode Exit fullscreen mode

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



trunk --version


Enter fullscreen mode Exit fullscreen mode

构建 Rust 全栈 Web 应用程序

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



cargo new frontend --vcs none


Enter fullscreen mode Exit fullscreen mode

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



[package]
name = "frontend"
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"


Enter fullscreen mode Exit fullscreen mode
  • yew是 Yew 框架(用于构建客户端 Web 应用程序的 Rust 框架)
  • wasm-bindgen是一个促进 WebAssembly 和 JavaScript 之间通信的库
  • web-sys是一个提供 Web API 绑定的库
  • gloo是一个为 WebAssembly 提供实用程序的库
  • wasm-bindgen-futures是一个提供在 WebAssembly 中使用 Future 的实用程序的库
  • serde是一个序列化/反序列化库
  • serde_json是一个 JSON 序列化/反序列化库

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



<!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>


Enter fullscreen mode Exit fullscreen mode
  • 我们在 HTML 文件的头部导入 Tailwind CSS CDN
  • 我们创建一个带有 id 的 div,appYew 应用将安装在该div 上
  • 我们导入frontend.jsTrunk 生成的文件

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



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

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

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


Enter fullscreen mode Exit fullscreen mode
  • 我们导入必要的依赖项
  • 我们定义一个User表示用户数据的结构
  • 我们定义main渲染 Yew 应用程序的函数

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

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

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

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

  • get_users:从后端 API 获取用户列表并更新用户状态。如果请求失败,则会设置错误消息。
  • create_user:发送 POST 请求,使用 user_state 中的数据创建新用户。成功后,触发 get_users 回调刷新用户列表。
  • update_user:通过向后端发送 PUT 请求来更新现有用户的信息。如果成功,则刷新用户列表并重置 user_state。
  • delete_user:根据用户 ID 向后端发送 DELETE 请求,删除用户。成功后,刷新用户列表。
  • 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("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(());
                        }

                        _ => 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)));
            }
        })
    };
...


Enter fullscreen mode Exit fullscreen mode

您可以在视频的这一部分逐行检查代码的编写

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

如果您了解 React,这与 JSX 文件中发生的情况类似。

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

  • 使用 Tailwind CSS 的主容器带有一些填充和漂亮的布局。
  • 顶部的大标题是“用户管理”,让用户了解该应用程序的用途。
  • 两个输入字段:一个用于输入用户名,另一个用于输入邮箱地址。输入时,它会更新状态以跟踪输入的内容。
  • 一个按钮,根据您是创建新用户还是更新现有用户,其操作和标签会发生变化。它会指示Create User您是添加新用户还是Update User编辑用户。
  • 在输入字段下方显示消息(如成功或错误消息)的空间(文本颜色始终为绿色;如果出现错误,请随意将其变为红色)。
  • 单击该按钮Fetch User List后,将从后端提取最新的用户数据。
  • 该部分列出了从后端获取的所有用户,并显示他们的 ID、姓名和电子邮件。
  • 列表中的每个用户都有一个“删除”按钮来删除他们,还有一个“编辑”按钮来将他们的详细信息加载到输入字段中进行编辑。


...
html! {
        <div class="container mx-auto p-4">
            <h1 class="text-4xl font-bold text-blue-500 mb-4">{ "User Management" }</h1>
                <div class="mb-4">
                    <input
                        placeholder="Name"
                        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="Email"
                        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() { "Update User" } else { "Create User" } }

                    </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"
                >
                    { "Fetch User List" }
                </button>

                <h2 class="text-2xl font-bold text-gray-700 mb-2">{ "User List" }</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: {}, Name: {}, Email: {}", 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"
                                >
                                    { "Delete" }
                                </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"
                                >
                                    { "Edit" }
                                </button>
                            </li>
                        }
                    })}

                </ul>


        </div>
    }
...


Enter fullscreen mode Exit fullscreen mode

您可以在视频的这一部分逐行检查代码的编写

以下是该文件的完整代码/frontend/src/main.rs



use yew::prelude::*;
use serde::{ Deserialize, Serialize };
use gloo::net::http::Request;
use 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(());
                        }

                        _ => 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">{ "User Management" }</h1>
                <div class="mb-4">
                    <input
                        placeholder="Name"
                        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="Email"
                        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() { "Update User" } else { "Create User" } }

                    </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"
                >
                    { "Fetch User List" }
                </button>

                <h2 class="text-2xl font-bold text-gray-700 mb-2">{ "User List" }</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: {}, Name: {}, Email: {}", 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"
                                >
                                    { "Delete" }
                                </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"
                                >
                                    { "Edit" }
                                </button>
                            </li>
                        }
                    })}

                </ul>


        </div>
    }
}

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

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


Enter fullscreen mode Exit fullscreen mode

构建前端

现在是时候运行前端了。

您可以输入:



cargo build --target wasm32-unknown-unknown


Enter fullscreen mode Exit fullscreen mode

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



trunk serve


Enter fullscreen mode Exit fullscreen mode

您现在可以访问http://127.0.0.1:8080并单击Fetch User List按钮从后端获取用户:

构建 Rust 全栈 Web 应用程序

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

例如,我们可以创建一个具有姓名yew和电子邮件的用户yes@mail.com

构建 Rust 全栈 Web 应用程序

用户应该在前端正确显示以下消息User created successfully

构建 Rust 全栈 Web 应用程序

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

构建 Rust 全栈 Web 应用程序

我们还可以更新用户,例如 ID 为 3 的用户,将名称更改为subscribe,将电子邮件更改为subscribe@mail.com。请注意,当我们点击Edit按钮时,表单将填充用户数据,按钮标签将更改为Update User

构建 Rust 全栈 Web 应用程序

点击Update User按钮后,我们应该看到以下消息User updated successfully

构建 Rust 全栈 Web 应用程序

最后一个测试是删除一个用户,例如 ID 为 3 的用户。点击Delete按钮后,我们应该看到以下消息User deleted successfully

构建 Rust 全栈 Web 应用程序

点击Delete按钮后,我们应该看到以下消息User deleted successfully

构建 Rust 全栈 Web 应用程序

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

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

构建 Rust 全栈 Web 应用程序

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

构建 Rust 全栈 Web 应用程序

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

构建 Rust 全栈 Web 应用程序

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



\dt
select * from users;


Enter fullscreen mode Exit fullscreen mode

构建 Rust 全栈 Web 应用程序

做得好!

结论

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

如果您更喜欢视频版本:

所有代码均可在GitHub上找到(链接见视频说明)

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

鏂囩珷鏉ユ簮锛�https://dev.to/francescoxx/rust-fullstack-web-app-wasm-yew-rocket-3ian
PREV
为什么 Rust 可能是当今开发人员的明智选择?
NEXT
使用 Spring Boot、Hibernate、Postgres、Docker 和 Docker Compose 的 Java CRUD Rest API