Rust 服务器、服务和应用程序:9 使用表单进行课程维护

Rust 服务器、服务和应用程序:9 使用表单进行课程维护

首页游戏大全Rust Red更新时间:2024-05-09
本章涵盖

在上一章中,我们研究了导师的注册。您可能还记得,当用户注册为导师时,有关导师的信息存储在两个数据库中。导师的个人资料详细信息(如姓名图像专业领域)保存在后端导师 Web 服务内的数据库中。用户的注册详细信息(如用户标识密码)存储在 Web 应用程序的本地数据库中。

在本章中,我们将在上一章的代码之上进行构建。我们将学习编写一个 Rust 前端 Web 应用程序,该应用程序允许用户登录到应用程序、与本地数据库交互以及与后端 Web 服务通信

请注意,本章的重点不是为Web应用程序编写HTML/javascript用户界面(因为这不是本书的重点)。相反,我们将专注于编写在 Rust 中构成 Web 应用程序的所有其他组件,包括路由请求处理程序和数据模型,并学习如何在后端 Web 服务上调用 API。代替用户界面,我们将从命令行 HTTP 工具测试 Web 应用程序的 API。使用 Tera 模板为 Web 应用程序编写基于 HTML/javascript 的 UI 的任务主要留给读者作为练习。

让我们首先从导师登录(身份验证)功能开始。

9.1 设计用户身份验证

对于导师登录,我们将接受两个字段 - 用户名和密码,并使用它来向 Web 应用程序验证导师

图 1 显示了导师登录窗体。

图 9.1.导师登录表单

现在,让我们看一下图 2 中导师登录的工作流。请注意,图 2 中的术语 Actix Web 服务器是指前端 Web 应用程序服务器,而不是后端导师 Web 服务

图 9.2.导师登录流

  1. 用户访问着陆页网址。将显示导师登录表单
  2. 用户名和密码的基本验证是使用 HTML 功能在表单本身内执行的,而无需向 Web Actix 服务器发送请求。
  3. 如果验证中存在错误,则会向用户提供反馈。
  4. 用户提交登录表单POST 请求将发送到登录路由上的 Actix Web 服务器,然后该服务器将请求路由到相应的路由处理程序
  5. 路由处理程序函数通过从本地数据库检索用户凭据来验证用户名和密码。
  6. 如果身份验证不成功,则会向用户显示登录表单,并显示相应的错误消息。错误消息的示例包括不正确的用户名或密码。
  7. 如果用户身份验证成功,用户将被定向到导师 Web 应用程序的主页。

现在我们已经清楚了本章将要开发的内容,让我们设置项目代码结构和基本脚手架。

9.2 设置项目结构

首先克隆第 8 章中的 ezytutors 存储库。

然后,让我们将PROJECT_ROOT环境变量设置为 /path-to-folder/ezytutors/tutor-web-app-ssr。此后,我们将此文件夹称为 $PROJECT_ROOT。

让我们在项目根目录下组织代码,如下所示:

  1. 复制文件夹 $PROJECT_ROOT/src/iter5,并将其重命名为 $PROJECT_ROOT/src/iter6
  2. 复制文件夹 $PROJECT_ROOT/static/iter5,并将其重命名为 $PROJECT_ROOT/static/iter6。此文件夹将包含 html/tera 模板。
  3. 复制文件 $PROJECT_ROOT/src/bin/iter5-ssr.rs,并将其重命名为 $PROJECT_ROOT/src/bin/iter6-ssr.rs。此文件包含 main() 函数,该函数将配置和启动 Actix Web 服务器(为我们正在构建的 Web 应用程序提供服务)。在 iter6-ssr.rs 中,将所有对 iter5 的引用替换为 iter6

还要确保为 HOST_PORTDATABASE_URL环境变量正确配置 $PROJECT_ROOT 中的 .env 文件。

我们已准备好开始编码。

让我们从 $PROJECT_ROOT/src/iter6/routes.rs 中的路由定义开始。

use crate::handler::{handle_register, show_register_form, show_signin_form, [CA]handle_signin}; #1 use actix_files as fs; use actix_web::web; pub fn app_config(config: &mut web::ServiceConfig) { config.service( web::scope("") .service(fs::Files::new("/static", "./static").show_files_listing()) .service(web::resource("/").route(web::get().to(show_register_form))) .service(web::resource("/signinform").route(web::get().to( [CA]show_signin_form))) #2 .service(web::resource("/signin").route(web::post().to( [CA]handle_signin))) #3 .service(web::resource("/register").route(web::post().to( [CA]handle_register))), ); }

有了这个,我们可以继续在 $PROJECT_ROOT/src/iter6/model.rs 中进行模型定义。

TutorSigninForm 数据结构添加到 model.rs

// Form to enable tutors to sign in #[derive(Serialize, Deserialize, Debug)] pub struct TutorSigninForm { #1 pub username: String, pub password: String, }

通过项目设置的基本结构,我们现在可以开始编写用于登录用户的代码。

9.3 实现用户身份验证

定义路由和数据模型后,让我们在 $PROJECT_ROOT/src/iter6/handler/auth.rs 中编写用于登录用户的处理程序函数。

首先,对导入进行以下更改:

use crate::model::{TutorRegisterForm, TutorResponse, TutorSigninForm, User}; #1

将以下处理程序函数添加到同一文件。在此文件中,将对 iter5 的引用替换为 iter6

pub async fn show_signin_form(tmpl: web::Data<tera::Tera>) -> [CA]Result<HttpResponse, Error> { #1 let mut ctx = tera::Context::new(); ctx.insert("error", ""); ctx.insert("current_name", ""); ctx.insert("current_password", ""); let s = tmpl .render("signin.html", &ctx) .map_err(|_| EzyTutorError::TeraError( [CA]"Template error".to_string()))?; Ok(HttpResponse::Ok().content_type("text/html").body(s)) } pub async fn handle_signin( #2 tmpl: web::Data<tera::Tera>, app_state: web::Data<AppState>, params: web::Form<TutorSigninForm>, ) -> Result<HttpResponse, Error> { Ok(HttpResponse::Ok().finish()) }

回想一下,调用show_signin_form处理程序函数是为了响应到达路由 /signinform 的请求,如路由定义中所定义。

让我们以 html 形式设计实际的登录。当用户选择登录EzyTutor Web应用程序时,将显示此表单。在 $PROJECT_ROOT/static/iter6 下创建一个新的文件 signin.html 文件,并向其中添加以下内容。请注意,应该已经有另一个文件寄存器.html已经存在于同一个文件夹中。

清单 9.1.导师登录表单

<!doctype html> <html> <head> <meta charset=utf-8> <title>Tutor registration</title> <style> #1 .header { padding: 20px; text-align: center; background: #fad980; color: rgb(48, 40, 43); font-size: 30px; } .center { margin: auto; width: 20%; min-width: 150px; border: 3px solid #ad5921; padding: 10px; } body, html { height: 100%; margin: 0; font-kerning: normal; } h1 { text-align: center; } p { text-align: center; } div { text-align: center; } div { background-color: rgba(241, 235, 235, 0.719); } body { background-image: url('/static/background.jpg'); background-repeat: no-repeat; background-attachment: fixed; background-size: cover; height: 500px; } #button1, #button2 { display: inline-block; } #footer { position: fixed; padding: 10px 10px 0px 10px; bottom: 0; width: 100%; /* Height of the footer*/ height: 20px; } </style> </head> <body> <div class="header"> <h1>Welcome to EzyTutor</h1> <p>Start your own online tutor business in a few minutes</p> </div> <div class="center"> <h2> Tutor sign in </h2> <form action=/signin method=POST> #2 <label for="userid">Enter username</label><br> #3 <input type="text" name="username" autocomplete="username" [CA]value="{{current_name}}" minlength="6" maxlength="12" required><br> <label for="password">Enter password</label><br> #4 <input type="password" name="password" [CA]autocomplete="new-password" value="{{current_password}}" minlength="8" maxlength="12" required><br> <label for="error"> #5 <p style="color:red">{{error}}</p> </label><br> <button type=submit id="button2">Sign in</button> #6 </form> <form action=/ method=GET> #7 <button type=submit id="button2">Register</button> </form> </div> <p> <div id="footer"> (c)Photo by Author </div> </p> </html>

将另一个文件用户.html添加到 $PROJECT_ROOT/static/iter6。这将在用户成功登录后显示。

清单 9.2.用户通知屏幕

<!DOCTYPE html> <html> <head> <meta charset=\"utf-8\" /> <title>{{title}}</title> </head> <body> <h1>Hi, {{name}}!</h1> <p>{{message}}</p> </body> </html>

最后,让我们看看 $PROJECT_ROOT/src/bin/iter6-ssr.rs__ 中的 main() 函数。将其修改为如下所示:

以下是导入:

#[path = "../iter6/mod.rs"] mod iter6; use actix_web::{web, App, HttpServer}; use actix_web::web::Data; use dotenv::dotenv; use iter6::{dbaccess, errors, handler, model, routes, state}; use routes::app_config; use sqlx::postgres::PgPool; use std::env; use tera::Tera;

而且,这是main()函数:

清单 9.3.main() 函数

#[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); //Start HTTP server let host_port = env::var("HOST_PORT").expect( [CA]"HOST:PORT address is not set in .env file"); println!("Listening on: {}", &host_port); let database_url = env::var("DATABASE_URL").expect( [CA]"DATABASE_URL is not set in .env file"); let db_pool = PgPool::connect(&database_url).await.unwrap(); // Construct App State let shared_data = web::Data::new(state::AppState { db: db_pool }); HttpServer::new(move || { let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), [CA]"/static/iter6/**/*")).unwrap(); App::new() .app_data(Data::new(tera)) .app_data(shared_data.clone()) .configure(app_config) }) .bind(&host_port)? .run() .await }

我们现在可以测试了。从 $PROJECT_ROOT 运行以下命令。

cargo run --bin iter6-ssr

注意:如果您收到错误:没有针对 'u32 - usize 的实现

运行以下命令:

cargo update -p lexical-core

从浏览器中,访问以下路由:

localhost:8080/signinform

您应该能够看到登录表单。您还可以通过访问显示注册表单的索引路由 / 以及使用显示的按钮切换到登录表单来调用签名表单。

完成此操作后,即可实现用于登录用户的逻辑。将以下内容添加到 $PROJECT_ROOT/src/iter6/handler.rs。不要忘记删除具有之前创建的相同名称的占位符函数。

清单 9.4.用于登录的处理程序函数

pub async fn handle_signin( tmpl: web::Data<tera::Tera>, app_state: web::Data<AppState>, params: web::Form<TutorSigninForm>, ) -> Result<HttpResponse, Error> { let mut ctx = tera::Context::new(); let s; let username = params.username.clone(); let user = get_user_record(&app_state.db, username.to_string()).await; #1 if let Ok(user) = user { let does_password_match = argon2::verify_encoded( &user.user_password.trim(), params.password.clone().as_bytes(), ) .unwrap(); if !does_password_match { #2 ctx.insert("error", "Invalid login"); ctx.insert("current_name", ¶ms.username); ctx.insert("current_password", ¶ms.password); s = tmpl .render("signin.html", &ctx) .map_err(|_| EzyTutorError::TeraError( [CA]"Template error".to_string()))?; } else { #3 ctx.insert("name", ¶ms.username); ctx.insert("title", &"Signin confirmation!".to_owned()); ctx.insert( "message", &"You have successfully logged in to EzyTutor!".to_owned(), ); s = tmpl .render("user.html", &ctx) .map_err(|_| EzyTutorError::TeraError( [CA]"Template error".to_string()))?; } } else { #4 ctx.insert("error", "User id not found"); ctx.insert("current_name", ¶ms.username); ctx.insert("current_password", ¶ms.password); s = tmpl .render("signin.html", &ctx) .map_err(|_| EzyTutorError::TeraError( [CA]"Template error".to_string()))?; }; Ok(HttpResponse::Ok().content_type("text/html").body(s)) }

Let’s test the signin function now. Run the following command from $PROJECT_ROOT.

cargo run --bin iter6-ssr

从浏览器中,访问以下路由:

localhost:8080/signinform

输入正确的用户名和密码。您应该会看到确认消息。

再次加载登录表单,这次为有效用户名输入错误的密码。验证是否收到错误消息。

尝试第三次输入表单,这次使用无效的用户名。同样,您应该会看到一条错误消息。

至此,我们结束本节。到目前为止,我们已经了解了如何使用Tera模板库定义模板来生成动态网页,以及如何向用户显示注册登录表单。我们还实现了用于注册登录用户以及处理用户输入中的错误的代码。我们还定义了自定义错误类型来统一错误处理。

现在让我们继续管理课程详细信息。

9.4 路由 HTTP 请求

在本节中,我们将添加导师维护课程的功能。

我们目前将所有处理程序函数都放在一个文件中。我们现在还必须添加处理程序以进行课程维护。因此,让我们首先将处理程序函数组织到它自己的模块中,以便能够跨多个源文件拆分处理程序函数。

首先在 $PROJECT_ROOT/src/iter6 下创建一个新的处理程序文件夹。

将$PROJECT_ROOT/src/iter6/handler.rs移动到$PROJECT_ROOT/src/iter6/handler中,并将其重命名为 auth.rs,因为这涉及注册和登录功能。(即 mv $PROJECT_ROOT/src/iter6/handler.rs $PROJECT_ROOT/src/iter6/handler/auth.rs in linux)。

$PROJECT_ROOT/src/iter6/handler 文件夹下 course.rs 和 mod.rs 创建新文件。在 mod.rs 中添加以下代码来构建处理程序文件夹中的文件,并将它们导出为 Rust 模块。

pub mod auth; #1 pub mod course; #2

修改 $PROJECT_ROOT/src/iter6/routes.rs,如下所示:

清单 9.5.添加路线维护路线

use crate::handler::auth::{handle_register, handle_signin, [CA]show_register_form, show_signin_form}; #1 use crate::handler::course::{handle_delete_course, handle_insert_course, [CA]handle_update_course}; #2 use actix_files as fs; use actix_web::web; pub fn app_config(config: &mut web::ServiceConfig) { #3 config.service( web::scope("") .service(fs::Files::new("/static", "./static").show_files_listing()) .service(web::resource("/").route(web::get().to(show_register_form))) .service(web::resource("/signinform").route(web::get().to( [CA]show_signin_form))) .service(web::resource("/signin").route(web::post().to( [CA]handle_signin))) .service(web::resource("/register").route(web::post().to( [CA]handle_register))), ); } pub fn course_config(config: &mut web::ServiceConfig) { #4 config.service( web::scope("/courses") #5 .service(web::resource("new/{tutor_id}").route(web::post().to( [CA]handle_insert_course))) #6 .service( #7 web::resource("{tutor_id}/{course_id}").route(web::put().to( [CA]handle_update_course)), ) .service( #8 web::resource("delete/{tutor_id}/{course_id}") .route(web::delete().to(handle_delete_course)), ), ); }

请注意,在我们指定 {tutor_id} 和 {course_id} 作为路径参数的地方,可以在 Actix Web 框架提供的提取器的帮助下从请求的路径中提取它们。

另外,请确保在 $PROJECT_ROOT/bin/iter6-ssr.rs 中添加新的课程维护路由,如下所示:

对导入语句:应用名称进行以下更改:

use routes::{app_config, course_config};

main() 函数中,进行更改以添加course_config路由。

HttpServer::new(move || { let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), [CA]"/static/iter6/**/*")).unwrap(); App::new() .app_data(Data::new(tera)) .app_data(shared_data.clone()) .configure(course_config) #1 .configure(app_config) #2 }) .bind(&host_port)? .run() .await

接下来,让我们现在在 $PROJECT_ROOT/src/iter6/handler/course.rs 中添加用于课程维护的占位符处理程序函数。稍后我们将编写实际逻辑来调用后端 Web 服务。

清单 9.6.课程维护处理程序函数的占位符

use actix_web::{web, Error, HttpResponse, Result}; use crate::state::AppState; pub async fn handle_insert_course( _tmpl: web::Data<tera::Tera>, _app_state: web::Data<AppState>, ) -> Result<HttpResponse, Error> { println!("Got insert request"); Ok(HttpResponse::Ok().body("Got insert request")) } pub async fn handle_update_course( _tmpl: web::Data<tera::Tera>, _app_state: web::Data<AppState>, ) -> Result<HttpResponse, Error> { Ok(HttpResponse::Ok().body("Got update request")) } pub async fn handle_delete_course( _tmpl: web::Data<tera::Tera>, _app_state: web::Data<AppState>, ) -> Result<HttpResponse, Error> { Ok(HttpResponse::Ok().body("Got delete request")) }

正如您将注意到的,处理程序函数现在除了返回消息外什么都不做。我们将在本章后面实现预期的处理程序功能。

请注意在变量名称前使用下划线 (_)。这是因为,我们还没有在处理程序函数的主体中使用这些参数,因此在变量名称之前添加下划线将防止编译器警告。

让我们对这四条路线进行快速测试:

使用以下命令运行服务器:

cargo run --bin iter6-ssr

要测试 POSTPUTDELETE 请求,请从命令行尝试以下操作:

curl -H "Content-Type: application/json" -X POST -d '{}' [CA]localhost:8080/courses/new/1 curl -H "Content-Type: application/json" -X PUT -d '{}' [CA]localhost:8080/courses/1/2 curl -H "Content-Type: application/json" -X DELETE -d '{}' [CA]localhost:8080/courses/delete/1/2

您应该看到从服务器返回的以下消息,对应于上面显示的三个 HTTP 请求:

Got insert request Got update request Got delete request

现在,我们已验证路由是否已正确建立,并且 HTTP 请求正在路由到正确的处理程序函数。在下一节中,让我们实现在处理程序函数中为导师添加课程的实际逻辑。

9.5 使用 HTTP POST 方法创建资源

在本部分中,我们将通过向后端导师 Web 服务发送 API 请求,为给定导师添加新课程。

转到第 6 章的代码存储库(即 /path-to-chapter4-folder/ezytutors/tutor-db ),并使用以下命令启动 tutor Web 服务

cargo run --bin iter5

导师 Web 服务现在应该已准备好接收来自导师 Web 应用程序的请求。现在让我们在 Web 应用程序中编写课程处理程序的代码,在 $PROJECT_ROOT/src/iter6/handler/course.rs 中。

修改 $PROJECT_ROOT/src/iter6/model.rs 以添加以下内容:

清单 9.7.课程维护的数据模型更改

#[derive(Deserialize, Debug, Clone)] pub struct NewCourse { #1 pub course_name: String, pub course_description: String, pub course_format: String, pub course_duration: String, pub course_structure: Option<String>, pub course_price: Option<i32>, pub course_language: Option<String>, pub course_level: Option<String>, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct NewCourseResponse { #2 pub course_id: i32, pub tutor_id: i32, pub course_name: String, pub course_description: String, pub course_format: String, pub course_structure: Option<String>, pub course_duration: String, pub course_price: Option<i32>, pub course_language: Option<String>, pub course_level: Option<String>, pub posted_time: String, } impl From<web::Json<NewCourseResponse>> for NewCourseResponse { #3 fn from(new_course: web::Json<NewCourseResponse>) -> Self { NewCourseResponse { tutor_id: new_course.tutor_id, course_id: new_course.course_id, course_name: new_course.course_name.clone(), course_description: new_course.course_description.clone(), course_format: new_course.course_format.clone(), course_structure: new_course.course_structure.clone(), course_duration: new_course.course_duration.clone(), course_price: new_course.course_price, course_language: new_course.course_language.clone(), course_level: new_course.course_level.clone(), posted_time: new_course.posted_time.clone(), } } }

此外,请确保添加以下模块导入,这是 From 特征实现所必需的。

use actix_web::web;

接下来,让我们重写处理程序函数以创建新课程。在 $PROJECT_ROOT/src/iter6/handler/course.rs 中,添加以下模块导入:

use actix_web::{web, Error, HttpResponse, Result}; #1 use crate::state::AppState; use crate::model::{NewCourse, NewCourseResponse, UpdateCourse, UpdateCourseResponse}; #2 use serde_json::json; #3 use crate::state::AppState;

然后修改handle_insert_course处理程序函数,如下所示:

清单 9.8.用于插入新课程的处理程序函数

pub async fn handle_insert_course( #1 _tmpl: web::Data<tera::Tera>, #2 _app_state: web::Data<AppState>, #3 path: web::Path<i32>, params: web::Json<NewCourse>, #4 ) -> Result<HttpResponse, Error> { let tutor_id = path.into_inner(); #5 let new_course = json!({ #6 "tutor_id": tutor_id, "course_name": ¶ms.course_name, "course_description": ¶ms.course_description, "course_format": ¶ms.course_format, "course_structure": ¶ms.course_structure, "course_duration": ¶ms.course_duration, "course_price": ¶ms.course_price, "course_language": ¶ms.course_language, "course_level": ¶ms.course_level }); let awc_client = awc::Client::default(); #7 let res = awc_client #8 .post("http://localhost:3000/courses/") .send_json(&new_course) .await .unwrap() .body() .await?; println!("Finished call: {:?}", res); let course_response: NewCourseResponse = serde_json::from_str( [CA]&std::str::from_utf8(&res)?)?; #9 Ok(HttpResponse::Ok().json(course_response)) #10 }

从 $PROJECT_ROOT 构建并运行 Web ssr 客户端,如下所示:

cargo run --bin iter6-ssr

让我们使用 curl 请求测试新课程的创建。确保导师 Web 服务正在运行。从另一个终端,运行以下命令:

curl -X POST localhost:8080/courses/new/1 -d '{"course_name":"Rust web [CA]development", "course_description":"Teaches how to write web apps in [CA]Rust", "course_format":"Video", "course_duration":"3 hours", [CA]"course_price":100}' -H "Content-Type: application/json"

通过在导师 Web 服务上运行 GET 请求来验证是否已添加新课程:

curl localhost:3000/courses/1

您应该在检索到 tutor-id = 1 的课程列表中看到新课程。

在下一节中,我们将编写处理程序函数来更新课程。

9.6 使用 HTTP PUT 方法更新资源

让我们在 $PROJECT_ROOT/src/iter6/model.rs 文件中编写用于更新课程的数据结构。

清单 9.9.更新课程的数据模型更改

// Update course #[derive(Deserialize, Serialize, Debug, Clone)] pub struct UpdateCourse { #1 pub course_name: Option<String>, pub course_description: Option<String>, pub course_format: Option<String>, pub course_duration: Option<String>, pub course_structure: Option<String>, pub course_price: Option<i32>, pub course_language: Option<String>, pub course_level: Option<String>, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct UpdateCourseResponse { #2 pub course_id: i32, pub tutor_id: i32, pub course_name: String, pub course_description: String, pub course_format: String, pub course_structure: String, pub course_duration: String, pub course_price: i32, pub course_language: String, pub course_level: String, pub posted_time: String, } impl From<web::Json<UpdateCourseResponse>> for UpdateCourseResponse { #3 fn from(new_course: web::Json<UpdateCourseResponse>) -> Self { UpdateCourseResponse { tutor_id: new_course.tutor_id, course_id: new_course.course_id, course_name: new_course.course_name.clone(), course_description: new_course.course_description.clone(), course_format: new_course.course_format.clone(), course_structure: new_course.course_structure.clone(), course_duration: new_course.course_duration.clone(), course_price: new_course.course_price, course_language: new_course.course_language.clone(), course_level: new_course.course_level.clone(), posted_time: new_course.posted_time.clone(), } } }

您还会注意到,我们已经定义了类似的数据结构,用于创建课程(NewCourse,NewCourseResponse)和更新课程(UpdateCourseUpdateCourseResponse)。 是否可以通过在创建和更新操作中重用相同的结构来优化?在实际项目场景中,可能会进行一些优化。但是,为了编写此示例代码,我们假设创建新课程所需的必填字段集与更新课程所需的必填字段集不同(其中没有必填字段)。此外,分离用于创建和更新操作的数据结构可以在学习时更容易理解。

接下来,让我们重写处理程序函数以更新 $PROJECT_ROOT/src/iter6/handler/course.rs 中的课程详细信息。

清单 9.10.用于更新课程的处理程序函数

pub async fn handle_update_course( _tmpl: web::Data<tera::Tera>, _app_state: web::Data<AppState>, web::Path((tutor_id, course_id)): web::Path<(i32, i32)>, params: web::Json<UpdateCourse>, ) -> Result<HttpResponse, Error> { let update_course = json!({ #1 "course_name": ¶ms.course_name, "course_description": ¶ms.course_description, "course_format": ¶ms.course_format, "course_duration": ¶ms.course_duration, "course_structure": ¶ms.course_structure, "course_price": ¶ms.course_price, "course_language": ¶ms.course_language, "course_level": ¶ms.course_level, }); let awc_client = awc::Client::default(); #2 let update_url = format!("http://localhost:3000/courses/{}/{}", [CA]tutor_id, course_id); #3 let res = awc_client #4 .put(update_url) .send_json(&update_course) .await .unwrap() .body() .await?; let course_response: UpdateCourseResponse = serde_json::from_str( [CA]&std::str::from_utf8(&res)?)?; #5 Ok(HttpResponse::Ok().json(course_response)) }

确保导入与更新相关的结构,如下所示:

use crate::model::{NewCourse, NewCourseResponse, UpdateCourse, UpdateCourseResponse};

从 $PROJECT_ROOT 构建并运行 Web ssr 客户端,如下所示:

cargo run --bin iter6-ssr

让我们使用 curl 请求进行测试,以更新我们之前创建的课程。确保导师 Web 服务正在运行。在新终端中,运行以下命令。将导师 ID 和课程 ID 替换为之前创建的新课程的导师 ID 和课程 ID

curl -X PUT -d '{"course_name":"Rust advanced web development", [CA]"course_description":"Teaches how to write advanced web apps in Rust", [CA]"course_format":"Video", "course_duration":"4 hours", [CA]"course_price":100}' localhost:8080/courses/1/27 -H [CA]"Content-Type: application/json"

通过在导师 Web 服务上运行 GET 请求来验证课程详细信息是否已更新:

curl localhost:3000/courses/1

注意:将 course_id:1 替换为您更新课程的tutor_id的正确值。

您应该会看到更新的课程详细信息已反映。

让我们继续删除课程。

9.7 使用 HTTP DELETE 方法删除资源

让我们更新处理程序函数以删除 $PROJECT_ROOT/src/iter6/handler/course.rs 中的课程。

示例 9.11.用于删除课程的处理程序函数

pub async fn handle_delete_course( _tmpl: web::Data<tera::Tera>, _app_state: web::Data<AppState>, path: web::Path<(i32, i32)>, #1 ) -> Result<HttpResponse, Error> { let (tutor_id, course_id) = path.into_inner(); let awc_client = awc::Client::default(); #2 let delete_url = format!("http://localhost:3000/courses/{}/{}", [CA]tutor_id, course_id); #3 let _res = awc_client.delete(delete_url).send().await.unwrap(); #4 Ok(HttpResponse::Ok().body("Course deleted")) #5 }

从 $PROJECT_ROOT 生成并运行导师 Web 应用,如下所示:

cargo run --bin iter6-ssr

运行删除请求,如下所示:

curl -X DELETE localhost:8080/courses/delete/1/19

tutor_idcourse_id替换为您自己的。

通过在导师 Web 服务上运行查询来验证课程是否已被删除。

curl localhost:3000/courses/1

tutor_id替换为您自己的。您应该看到该课程已在_tutor Web 服务中删除。

有了这个,我们已经看到了如何从用 Rust 编写的 Web 客户端前端添加、更新和删除课程。

作为练习,读者可以执行以下附加任务:

  1. 实现新路由以检索导师的课程列表
  2. 创建用于创建、更新和删除课程的 HTML/Tera 模板
  3. 为具有无效用户输入的情况添加其他错误处理。
9.8 小结

至此,我们得出本章的结论,以及关于 Rust Web 应用程序开发的这一部分。

在下一章中,我们将介绍与 Rust 中的异步服务器相关的高级主题。

下一章见。

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved