为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
WebApp

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

对于前端:

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

对于后端:

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

对于数据库:

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

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

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

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

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

架构

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

构建一个 Rust 全栈 web 应用

前端将使用 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 数据库)

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

构建一个 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: {}

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

  • 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,数据库很可能运行成功了。

构建一个 Rust 全栈 web 应用

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

    docker ps -a

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

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

构建一个 Rust 全栈 web 应用

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

    docker exec -it db psql -U postgres

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

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

    \dt

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

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

构建一个 Rust 全栈 web 应用

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

设置后端

我们将使用 Rocket 构建后端。

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

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

    cargo new backend --vcs none

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

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

构建一个 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 }

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

  • 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

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

我们应该看到以下输出:

构建一个 Rust 全栈 web 应用

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

构建一个全栈 Rust 网页应用

使用 Postman 测试 API

你可以使用 Postman 测试 API。

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

构建一个 Rust 全栈 web 应用

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

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

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

构建一个 Rust 全栈 web 应用

你可以再创建2个用户:

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

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

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

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

你应该看到以下输出:

构建一个 Rust 全栈 web 应用

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

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

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

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

构建一个 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;

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

构建一个 Rust 全栈 Web 应用

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

设置前端

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

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

    rustup target add wasm32-unknown-unknown

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

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

    cargo install trunk

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

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

    trunk --version

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

构建一个 Rust 全栈 web 应用

现在你可以创建一个名为 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 class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="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 并点击 获取用户列表 按钮从后端获取用户列表:

构建一个 Rust 全栈 web 应用

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

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

构建一个 Rust 全栈 web 应用

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

    \dt
    select * from users;

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

构建一个 Rust 全栈 web 应用

做得好!

结论

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

如果你更喜欢视频版本:

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

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

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消