Rust Web 全棧開發(十):編寫服務器端 Web 應用
- Rust Web 全棧開發(十):編寫服務器端 Web 應用
- 創建成員庫:webapp
- models
- handlers
- routers
- errors
- mod
- svr
- static
- teachers.html
- register.html
- bootstrap.min.css
- bootstrap.min.js
- jquery.min.js
- 測試
Rust Web 全棧開發(十):編寫服務器端 Web 應用
參考視頻:https://www.bilibili.com/video/BV1RP4y1G7KF
繼續之前的 Actix 項目。
我們已經實現了一個 Web Service,現在我們想創建一個 Web App,在網頁端查看并操作數據庫中的數據。
主要的技術是模板引擎,它的作用是以靜態頁面作為模板,把動態的數據渲染進頁面,最后一同返回給用戶。
創建成員庫:webapp
回到頂層的工作空間,在終端執行命令 cargo new webapp,創建一個新的成員庫 webapp。
工作空間的 Cargo.toml 會自動添加這個新成員:
修改 webapp 中的 Cargo.toml,添加如下依賴:
[package]
name = "webapp"
version = "0.1.0"
edition = "2021"[dependencies]
actix-files = "0.6.0-beta.16"
actix-web = "4.0.0-rc.2"
awc = "3.0.0-beta.21"
dotenv = "0.15.0"
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
tera = "1.15.0"
在 webapp 目錄下新建一個 .env 文件,內容如下:
HOST_PORT=127.0.0.1:8080
webapp 目錄一覽:
├── webapp
│ ├── src
│ │ ├── bin
│ │ └── svr.rs
│ │ └── mod.rs
│ │ └── errors.rs
│ │ └── handlers.rs
│ │ └── models.rs
│ │ └── routers.rs
│ ├── static
│ │ ├── css
│ │ └── bootstrap.min.css
│ │ ├── javascript
│ │ └── bootstrap.min.js
│ │ └── jquery.min.js
│ │ └── register.html
│ │ └── teachers.html
│ ├── .env
│ └── Cargo.toml
models
編輯 models.rs:
use serde::{Deserialize, Serialize};/// 教師信息,用于應用注冊
#[derive(Serialize, Deserialize, Debug)]
pub struct TeacherRegisterForm {pub name: String,pub picture_url: String,pub profile: String,
}/// 教師信息,用于數據庫查詢
#[derive(Serialize, Deserialize, Debug)]
pub struct TeacherResponse {pub id: i32,pub name: String,pub picture_url: String,pub profile: String,
}
handlers
編輯 handlers.rs:
use crate::errors::MyError;
use crate::models::{TeacherRegisterForm, TeacherResponse};
use actix_web::{web, Error, HttpResponse, Result};
use serde_json::json;pub async fn get_all_teachers(tmpl: web::Data<tera::Tera>
) -> Result<HttpResponse, Error> {// 創建 HTTP 客戶端let awc_client = awc::Client::default();let res = awc_client.get("http://localhost:3000/teachers/").send().await.unwrap().json::<Vec<TeacherResponse>>().await.unwrap();// 創建一個上下文,可以向 HTML 模板里添加數據let mut ctx = tera::Context::new();// 向上下文中插入數據ctx.insert("error", "");ctx.insert("teachers", &res);// s 是渲染的模板,靜態部分是 teachers.html,動態數據是 ctxlet s = tmpl.render("teachers.html", &ctx).map_err(|_| MyError::TeraError("Template error".to_string()))?;Ok(HttpResponse::Ok().content_type("text/html").body(s))
}pub async fn show_register_form(tmpl: web::Data<tera::Tera>
) -> Result<HttpResponse, Error> {let mut ctx = tera::Context::new();ctx.insert("error", "");ctx.insert("current_name", "");ctx.insert("current_picture_url", "");ctx.insert("current_profile", "");let s = tmpl.render("register.html", &ctx).map_err(|_| MyError::TeraError("Template error".to_string()))?;Ok(HttpResponse::Ok().content_type("text/html").body(s))
}pub async fn handle_register(tmpl: web::Data<tera::Tera>,params: web::Form<TeacherRegisterForm>,
) -> Result<HttpResponse, Error> {let mut ctx = tera::Context::new();let s;if params.name == "Dave" {ctx.insert("error", "Dave already exists!");ctx.insert("current_name", ¶ms.name);ctx.insert("current_picture_url", ¶ms.picture_url);ctx.insert("current_profile", ¶ms.profile);s = tmpl.render("register.html", &ctx).map_err(|err| MyError::TeraError(err.to_string()))?;} else {let new_teacher = json!({"name": ¶ms.name,"picture_url": ¶ms.picture_url,"profile": ¶ms.profile,});let awc_client = awc::Client::default();let res = awc_client.post("http://localhost:3000/teachers/").send_json(&new_teacher).await.unwrap().body().await?;let teacher_response: String =serde_json::from_str(&std::str::from_utf8(&res)?)?;s = format!("Message from Web Server: {}", teacher_response);}Ok(HttpResponse::Ok().content_type("text/html").body(s))
}
get_all_teachers 函數向 http://localhost:3000/teachers/ 發生 GET 請求,將得到的 Vec<TeacherResponse> 渲染到 teachers.html,最終返回一個包含渲染網頁的 HTTP Response。
show_register_form 函數創建一個包含教師信息(name、picture_url、profile)的上下文 ctx,渲染到 register.html,最終返回一個包含渲染網頁的 HTTP Response。
handle_register 函數判斷傳入的教師的 name 是否是 Dave 來做出不同的響應。如果是,則將 Dave already exists! 錯誤信息渲染到 register.html,作為 s。如果不是,則向 http://localhost:3000/teachers/ 發送 json 化的新教師信息,將服務器的響應作為 s。最終返回一個包含 s 的 HTTP Response。
routers
編輯 routers.rs:
use crate::handlers::{get_all_teachers, handle_register, show_register_form};
use actix_files;
use actix_web::web;pub fn app_config(config: &mut web::ServiceConfig) {config.service(web::scope("").service(actix_files::Files::new("/static", "./static/").show_files_listing()).service(web::resource("/").route(web::get().to(get_all_teachers))).service(web::resource("/register").route(web::get().to(show_register_form))).service(web::resource("/register-post").route(web::post().to(handle_register))));
}
errors
編輯 errors.rs:
use actix_web::{error, http::StatusCode, HttpResponse, Result};
use serde::Serialize;
use std::fmt;#[derive(Debug, Serialize)]
pub enum MyError {ActixError(String),#[allow(dead_code)]NotFound(String),TeraError(String),
}#[derive(Debug, Serialize)]
pub struct MyErrorResponse {error_message: String,
}impl std::error::Error for MyError {}impl MyError {fn error_response(&self) -> String {match self {MyError::ActixError(msg) => {println!("Server error occurred: {:?}", msg);"Internal server error".into()}MyError::TeraError(msg) => {println!("Error in rendering the template: {:?}", msg);msg.into()}MyError::NotFound(msg) => {println!("Not found error occurred: {:?}", msg);msg.into()}}}
}impl error::ResponseError for MyError {fn status_code(&self) -> StatusCode {match self {MyError::ActixError(_msg) | MyError::TeraError(_msg) => StatusCode::INTERNAL_SERVER_ERROR,MyError::NotFound(_msg) => StatusCode::NOT_FOUND}}fn error_response(&self) -> HttpResponse {HttpResponse::build(self.status_code()).json(MyErrorResponse {error_message: self.error_response(),})}
}impl fmt::Display for MyError {fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {write!(f, "{:?}", self)}
}impl From<actix_web::error::Error> for MyError {fn from(err: actix_web::error::Error) -> Self {MyError::ActixError(err.to_string())}
}
mod
mod.rs:
pub mod models;
pub mod handlers;
pub mod routers;
pub mod errors;
svr
bin/svr.rs 類似于 teacher_service.rs。
#[path = "../mod.rs"]
mod webapp;use actix_web::{web, App, HttpServer};
use dotenv::dotenv;
use routers::app_config;
use std::env;
use webapp::{errors, handlers, models, routers};
use tera::Tera;#[actix_web::main]
async fn main() -> std::io::Result<()> {// 檢測并讀取 .env 文件中的內容,若不存在也會跳過異常dotenv().ok();let host_port = env::var("HOST_PORT").expect("HOST_PORT is not set in .env file");println!("Listening on {}", &host_port);let app = move || {let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static/**/*")).unwrap();App::new().app_data(web::Data::new(tera)).configure(app_config)};HttpServer::new(app).bind(&host_port)?.run().await
}
CARGO_MANIFEST_DIR 就是 webapp 目錄的地址,這一句代碼將該地址與 /static 連接起來,告訴 app 這些靜態文件的位置。
static
這個目錄下都是網頁相關的靜態文件。
├── static
│ ├── css
│ └── bootstrap.min.css
│ ├── javascript
│ └── bootstrap.min.js
│ └── jquery.min.js
│ └── register.html
│ └── teachers.html
teachers.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Teachers</title><link rel="stylesheet" href="/static/css/bootstrap.min.css">
</head><body><div class="container"><h1 class="mt-4 mb-4">Teacher List</h1><ul class="list-group">{% for t in teachers %}<li class="list-group-item"><div class="d-flex w-100 justify-content-between"><h5 class="mb-1">{{t.name}}</h5></div><p class="mb-1">{{t.profile}}</p></li>{% endfor %}</ul><div class="mt-4"><a href="/register" class="btn btn-primary">Register a Teacher</a></div></div><script src="/static/javascript/jquery.min.js"></script><script src="/static/javascript/bootstrap.min.js"></script>
</body></html>
register.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Teacher registration</title><link rel="stylesheet" href="/static/css/bootstrap.min.css">
</head><body style="background-color: #f2f9fd;"><div class="container"><h2 class="header text-center mb-4 pt-4">Teacher Registration</h2><div class="row justify-content-center"><div class="col-md-6"><form action="/register-post" method="POST" class="p-4 bg-white rounded shadow-sm"><div class="form-group"><label for="name">Teacher Name</label><input type="text" name="name" id="name" value="{{current_name}}" class="form-control"></div><div class="form-group"><label for="picture_url">Teacher Picture URL</label><input type="text" name="picture_url" id="picture_url" value="{{current_picture_url}}"class="form-control"></div><div class="form-group"><label for="profile">Teacher Profile</label><input type="text" name="profile" id="profile" value="{{current_profile}}" class="form-control"></div><div><p style="color: red">{{error}}</p></div><br/><button type="submit" id="button1" class="btn btn-primary">Register</button></form></div></div></div><script src="/static/javascript/jquery.min.js"></script><script src="/static/javascript/bootstrap.min.js"></script>
</body></html>
bootstrap.min.css
文件太長,見于:UestcXiye/Actix-Workspace/webapp/static/css/bootstrap.min.css
bootstrap.min.js
文件太長,見于:UestcXiye/Actix-Workspace/webapp/static/javascript/bootstrap.min.js
jquery.min.js
文件太長,見于:UestcXiye/Actix-Workspace/webapp/static/javascript/jquery.min.js
測試
因為路由中配置了 /static,訪問 http://localhost:8080/static 會給出項目 static 文件夾的目錄:
數據庫中 teacher 表:
在一個終端 cd 到 webservice,執行命令 cargo run。
在另一個終端 cd 到 webservice,執行命令 cargo run。
在瀏覽器訪問 http://localhost:8080,頁面如下:
webapp 終端輸出 Listening on 127.0.0.1:8080。
點擊 Register a Teacher 按鈕,跳轉到 http://localhost:8080/register,可以填寫表單,注冊新的教師信息。
我們先測試無法新增的情況,也就是 Teacher Name 是 Dave 的情況:
再測試可以插入新教師信息的情況:
插入成功,跳轉到 http://localhost:8080/register-post 頁面:
數據庫中 teacher 表新增一條教師信息: