Rust Web 全栈开发(十):编写服务器端 Web 应用

Rust Web 全栈开发(十):编写服务器端 Web 应用

Rust Web 全栈开发(十):编写服务器端 Web 应用

参考视频:https://www.bilibili.com/video/BV1RP4y1G7KF

继续之前的 Actix 项目。

我们已经实现了一个 Web Service,现在我们想创建一个 Web App,在网页端查看并操作数据库中的数据。

主要的技术是模板引擎,它的作用是以静态页面作为模板,把动态的数据渲染进页面,最后一同返回给用户。

创建成员库:webapp

回到顶层的工作空间,在终端执行命令 cargo new webapp,创建一个新的成员库 webapp。

工作空间的 Cargo.toml 会自动添加这个新成员:

修改 webapp 中的 Cargo.toml,添加如下依赖:

bash 复制代码
[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 目录一览:

bash 复制代码
├── 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

rust 复制代码
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

rust 复制代码
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,动态数据是 ctx
    let 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", &params.name);
        ctx.insert("current_picture_url", &params.picture_url);
        ctx.insert("current_profile", &params.profile);
        s = tmpl
            .render("register.html", &ctx)
            .map_err(|err| MyError::TeraError(err.to_string()))?;
    } else {
        let new_teacher = json!({
            "name": &params.name,
            "picture_url": &params.picture_url,
            "profile": &params.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

rust 复制代码
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

rust 复制代码
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

rust 复制代码
pub mod models;
pub mod handlers;
pub mod routers;
pub mod errors;

svr

bin/svr.rs 类似于 teacher_service.rs。

rust 复制代码
#[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

这个目录下都是网页相关的静态文件。

bash 复制代码
├── static
│   ├── css
│       └── bootstrap.min.css
│   ├── javascript
│       └── bootstrap.min.js
│       └── jquery.min.js
│   └── register.html
│   └── teachers.html

teachers.html

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

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 表新增一条教师信息:

相关推荐
小宁爱Python3 分钟前
TypeScript 泛型详解:从基础到实战应用
前端·javascript·typescript
wjpwjpwjp08318 分钟前
[MySQL基础3] 数据控制语言DCL和MySQL中的常用函数
数据库·笔记·后端·学习·mysql
东边有耳10 分钟前
一文深扒银行会计科目(收藏备查)
后端·架构·产品经理
鱼樱前端11 分钟前
一文讲解时下比较火的Rust语言之-rust开发环境搭建
前端·javascript
东边有耳16 分钟前
银行将你的存款放大了!!!
后端
白日依山尽yy26 分钟前
SpringBoot项目部署至云服务器
java·spring boot·后端
midsummer_woo31 分钟前
基于springboot+vue+mysql的作业管理系统(源码+论文)
vue.js·spring boot·mysql
Moment1 小时前
Next.js 15.4 正式发布:Turbopack 全面稳定,预热 Next.js 16 😍😍😍
前端·javascript·node.js
虚!!!看代码1 小时前
uni-app 跳转页面传参
前端·vue.js·uni-app
脑袋大大的1 小时前
UniApp 自定义导航栏:解决安全区域适配问题的完整实践
前端·javascript·安全·uni-app·uniapp·app开发·uniappx