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上找到(链接见视频说明)
建筑学
在我们开始之前,这里有一个我们要构建的应用程序的简单架构图:
前端将使用 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 .
从根文件夹初始化 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。
创建一个名为 的新 Rust 项目backend
,无需初始化 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 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)
}
在视频的这一部分,我解释了上面的代码。
解释
- 我们在文件顶部进行所有导入。我们还定义了一个
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
我们应该看到以下输出:
您可以访问以下 URL:http://127.0.0.1:8000/api/users
您应该会看到一个空数组[]
:
使用 Postman 测试 API
您可以使用 Postman 测试 API。
GET
您可以通过发送请求到以下地址来获取用户列表http://127.0.0.1:8000/api/users
:
您可以通过发送带有以下 JSON 主体的POST
请求来创建新用户:http://127.0.0.1:8000/api/users
{
"name": "AAA",
"email": "aaa@mail.com"
}
您可以再创建 2 个用户:
{
"name": "BBB",
"email": "
}
{
"name": "CCC",
"email": "
}
您应该看到以下输出:
要更新用户,您可以发送带有以下 JSON 主体的PUT
请求:http://127.0.0.1:8000/api/users/2
{
"name": "Francesco",
"email": "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 进行样式设置。所有这些代码都将编译为 Web Assembly 并在浏览器中运行。
重要!如果您从未在计算机上使用过 Wasm for Rust,则可以通过运行以下命令进行安装:
rustup target add wasm32-unknown-unknown
重要!您还必须trunk
在计算机上安装。您可以通过运行以下命令来安装:
cargo install trunk
您可以trunk
通过运行以下命令来验证是否已安装:
trunk --version
现在您可以创建一个名为的新 Rust 项目frontend
(确保在rustfs
文件夹中):
cargo new frontend --vcs none
现在打开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"
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>
- 我们在 HTML 文件的头部导入 Tailwind CSS CDN
- 我们创建一个带有 id 的 div,
app
Yew 应用将安装在该div 上 - 我们导入
frontend.js
Trunk 生成的文件
现在打开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();
}
- 我们导入必要的依赖项
- 我们定义一个
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)));
}
})
};
...
您可以在视频的这一部分逐行检查代码的编写
现在我们需要添加 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>
}
...
您可以在视频的这一部分逐行检查代码的编写
以下是该文件的完整代码/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();
}
构建前端
现在是时候运行前端了。
您可以输入:
cargo build --target wasm32-unknown-unknown
然后您可以通过运行以下命令来运行前端:
trunk serve
您现在可以访问http://127.0.0.1:8080
并单击Fetch User List
按钮从后端获取用户:
如您所见,我们从后端获取用户并将其显示在前端。前端还允许您创建、更新和删除用户。
例如,我们可以创建一个具有姓名yew
和电子邮件的用户yes@mail.com
:
用户应该在前端正确显示以下消息User created successfully
:
为了检查数据的一致性,我们可以使用 Postman 发出GET
请求http://127.0.0.1:8000/api/users
:
我们还可以更新用户,例如 ID 为 3 的用户,将名称更改为subscribe
,将电子邮件更改为subscribe@mail.com
。请注意,当我们点击Edit
按钮时,表单将填充用户数据,按钮标签将更改为Update User
:
点击Update User
按钮后,我们应该看到以下消息User updated successfully
:
最后一个测试是删除一个用户,例如 ID 为 3 的用户。点击Delete
按钮后,我们应该看到以下消息User deleted successfully
:
点击Delete
按钮后,我们应该看到以下消息User deleted successfully
:
注意:您应该能够在后端日志中看到所有 HTTP 请求。
让我们创建最后一个用户,并将其last
命名为电子邮件last@mail.com
如果我们使用 Postman 并向 发出GET
请求http://127.0.0.1:8000/api/users
,我们应该看到以下输出:
我们还可以通过打开新标签并访问来查看数据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
鏂囩珷鏉ユ簮锛�https://dev.to/francescoxx/rust-fullstack-web-app-wasm-yew-rocket-3ian