到本教程结束时,您将了解如何使用以下技术创建一个简单而完整的全栈应用程序:
对于前端:
- 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 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
并点击 获取用户列表
按钮从后端获取用户列表:
如你所见,我们从后端获取用户信息并在前端显示。前端还允许你创建、更新和删除用户。
例如,我们可以创建一个名为 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
共同学习,写下你的评论
评论加载中...
作者其他优质文章