领先的免费Web技术教程,涵盖HTML到ASP.NET

网站首页 > 知识剖析 正文

Rust Web编程:第十二章 在 Rocket 中重新创建我们的应用程序

nixiaole 2024-11-24 19:57:23 知识剖析 12 ℃

至此,我们已经使用 Actix Web 框架构建了一个功能齐全的待办事项应用程序。 在本章中,我们将介绍核心概念,以便在决定在 Rocket 中完全重新创建待办事项应用程序时不会有任何阻碍。 该框架可能会吸引一些开发人员,因为它不需要太多的样板代码。

在本章中,我们将充分利用隔离的模块化代码,通过复制并插入现有的模块、视图、数据库连接配置和测试管道,在一章中完全重新创建我们的应用程序。 即使您对在 Rocket 中构建 Web 应用程序不感兴趣,我仍然建议您仍然完成本章,因为您将体验到为什么执行良好解耦的测试和编写结构良好的代码很重要,因为良好的测试和结构将 使您能够毫不费力地切换 Web 框架。

在本章中,我们将讨论以下主题:

什么是Rocket?

设置我们的服务器

插入我们现有的模块

使用 JSON 返回状态

返回多种状态

向 Rocket 注册我们的观点

插入我们现有的测试

到本章结束时,您将在 Rocket 中以最少的编码获得一个完全可用的待办事项应用程序。 您不仅将了解配置和运行 Rocket 服务器的基础知识,而且还能够从使用 Actix Web 的其他代码库移植模块、视图和测试,并将它们插入您的 Rocket 服务器,反之亦然。 这不仅是一项宝贵的技能,而且还具体体现了对高质量、独立代码的需求。 您将亲眼目睹如何以及为什么应该按照我们的方式构建代码。

什么是Rocket?

Rocket 是一个 Rust Web 框架,类似于 Actix Web。 它比 Actix Web 更新,并且在撰写本文时用户群较少。 在本书的前一版本中,Rocket 在 nightly Rust 上运行,这意味着版本不稳定。 然而,现在,Rocket 正在稳定的 Rust 上运行。

该框架确实有一些优点,具体取决于您的编码风格。 Rocket 编写起来更简单,因为它本身实现了样板代码,因此开发人员不必自己编写样板代码。 Rocket 还支持开箱即用的 JSON 解析、表单和类型检查,这些都只需几行代码即可实现。 启动 Rocket 服务器后,诸如日志记录之类的功能就已经实现了。 如果您想轻松地启动应用程序,那么 Rocket 是一个很好的框架。 然而,它并不像 Actix Web 那样成熟,这意味着当您变得更高级时,您可能会发现自己羡慕 Actix Web 具有的一些功能和实现。 然而,在我多年的 Web 开发生涯中,我从未遇到过因框架的选择而严重阻碍的问题。 这主要取决于偏好。 为了真正感受到差异,有必要带火箭去试一试。 在下一节中,我们将创建一个基本服务器。

设置我们的服务器

当谈到在 Rocket 中设置基本服务器时,我们将从 main.rs 文件中定义的所有内容开始。 首先,启动一个新的 Cargo 项目,然后使用以下代码在 Cargo.toml 文件中定义 Rocket 依赖项:

[dependencies]
rocket = "0.5.0-rc.2"

这就是我们现在所需要的依赖关系。 现在,我们可以转到 src/main.rs 文件来定义应用程序。 最初,我们需要使用以下代码导入 Rocket crate 以及与 Rocket crate 关联的宏:

#[macro_use] extern crate rocket;

我们现在可以使用以下代码定义基本的 hello world 视图:

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

通过上面的代码,我们可以推断出函数之前的宏定义了方法和 URL 端点。 函数是调用视图时执行的逻辑,函数返回的内容就是返回给用户的内容。 为了感受 URL 宏的强大功能,我们可以再创建两个视图 - 一个表示“你好”,另一个表示“再见”:

#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}
#[get("/bye/<name>/<age>")]
fn bye(name: String, age: u8) -> String {
    format!("Goodbye, {} year old named {}!", age, name)
}

在这里,我们可以看到我们可以将参数从 URL 传递到函数中。 同样,这段代码清晰明了。 除了将这些视图附加到服务器并使用以下代码启动它之外,我们没有什么可做的:

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, hello, bye])
}

在这里,我们可以看到我们必须使用 Rocket 中的宏来装饰 main 函数,并且我们附加了我们定义的没有前缀的视图。 然后我们可以运行 Cargo run 命令来启动服务器。 完成运行命令后,我们会得到以下输出:

 Configured for debug.
   >> address: 127.0.0.1
   >> port: 8000
   >> workers: 8
   >> ident: Rocket
   >> limits: bytes = 8KiB, data-form = 2MiB, file = 1MiB, form =
   32KiB, json = 1MiB, msgpack = 1MiB, string = 8KiB
   >> temp dir: /var/folders/l7/q2pdx7lj0l72s0lsf3kc34fh0000gn/T/
   >> http/2: true
   >> keep-alive: 5s
   >> tls: disabled
   >> shutdown: ctrlc = true, force = true, signals = [SIGTERM],
   grace = 2s, mercy = 3s
   >> log level: normal
   >> cli colors: true
 Routes:
   >> (index) GET /
   >> (bye) GET /bye/<name>/<age>
   >> (hello) GET /hello/<name>/<age>
 Fairings:
   >> Shield (liftoff, response, singleton)
 Shield:
   >> X-Content-Type-Options: nosniff
   >> X-Frame-Options: SAMEORIGIN
   >> Permissions-Policy: interest-cohort=()
 Rocket has launched from http://127.0.0.1:8000

在这里,我们可以看到日志记录很全面。 它定义了服务器的端口、地址和配置。 然后它定义已连接的路线以及整流罩。 通过前面的日志记录,我们可以看到服务器运行状况良好,并且我们拥有预期的路由。 在这里,我们可以看到日志记录是开箱即用的。 与 Actix Web 不同,我们不必定义任何内容。 我们还会收到一条注释,说明安装了哪些视图以及服务器正在侦听的 URL。

现在我们可以在浏览器中调用我们的 hello 视图,它会提供以下输出:


调用此视图还会为我们提供以下日志:

GET /hello/maxwell/33 text/html:
   >> Matched: (hello) GET /hello/<name>/<age>
   >> Outcome: Success
   >> Response succeeded.

从日志来看,我们似乎不能再问了。 我们现在已经启动并运行了一个基本的服务器; 但是,它不具备我们之前在 Actix Web 中构建的应用程序中拥有的所有功能。 重新编码我们拥有的所有功能会导致章节过长。 在下一节中,我们将利用模块化代码并将所有功能放入 Rocket 应用程序中。

插入我们现有的模块

在整本书中,我们一直在自己的文件或目录中构建独立的模块,这些模块只关心一个进程。 例如,数据库文件仅专注于创建和管理数据库连接。 待办事项模块仅专注于构建待办事项,而 JSON 序列化模块完全关注将数据结构序列化为 JSON 或从 JSON 序列化数据结构。 考虑到这一切,我们将看到如何轻松地将这些模块复制到我们的应用程序中并使用。 一旦我们完成了这一点,您将直接理解为什么隔离模块很重要。

首先,我们必须使用以下代码在 Cargo.toml 文件中定义依赖项:

[dependencies]
rocket = {version = "0.5.0-rc.2", features = ["json"]}
bcrypt = "0.13.0"
serde_json = "1.0.59"
serde_yaml = "0.8.23"
chrono = {version = "0.4.19", features = ["serde"]}
serde = { version = "1.0.136", features = ["derive"] }
uuid = {version = "1.0.0", features = ["serde", "v4"]}
diesel = { version = "1.4.8", features = ["postgres",
                              "chrono", "r2d2"] }
lazy_static = "1.4.0"

这些是我们在之前的模块中使用过的crate。 现在,我们可以使用以下 Bash 命令将旧模块从 web_app 目录中的 Actix Web 应用程序复制到我们的 Rocket 应用程序:

cp -r ./web_app/src/json_serialization ./rocket_app/src/json_serialization
cp -r ./web_app/src/to_do ./rocket_app/src/to_do
cp -r ./web_app/src/models ./rocket_app/src/models
cp web_app/src/config.rs rocket_app/src/config.rs
cp web_app/config.yml rocket_app/config.yml
cp web_app/src/schema.rs rocket_app/src/schema.rs
cp ./web_app/src/database.rs ./rocket_app/src/database.rs
cp -r ./web_app/migrations ./rocket_app/migrations
cp ./web_app/docker-compose.yml ./rocket_app/docker-compose.yml
cp ./web_app/.env ./rocket_app/.env

一切都接近工作; 不过,我们确实有一些 Actix Web 框架的参考资料。 这些可以通过删除特征含义来删除。 如下图所示,可以直接引用孤立的模块,并可以使用特征来实现高级集成:


一旦我们删除了 src/database.rs 和 src/json_serialization/to_do_items.rs 文件中的 Actix Web 特征实现,我们就可以在 main.rs 文件中定义并导入我们的模块。 main.rs 文件的顶部应如下所示:

#[macro_use] extern crate rocket;
#[macro_use] extern crate diesel;
use diesel::prelude::*;
use rocket::serde::json::Json;
mod schema;
mod database;
mod json_serialization;
mod models;
mod to_do;
mod config;
use crate::models::item::item::Item;
use crate::json_serialization::to_do_items::ToDoItems;
use crate::models::item::new_item::NewItem;
use database::DBCONNECTION;

导入模块后,我们可以使用以下代码重新创建创建视图:

#[post("/create/<title>")]
fn item_create(title: String) -> Json<ToDoItems> {
    let db = DBCONNECTION.db_connection.get().unwrap();
    let items = schema::to_do::table
        .filter(schema::to_do::columns::title.eq(&title.as_str()))
        .order(schema::to_do::columns::id.asc())
        .load::<Item>(&db)
        .unwrap();
    if items.len() == 0 {
        let new_post = NewItem::new(title, 1);
        let _ = diesel::insert_into(schema::to_do::table)
                        .values(&new_post)
                        .execute(&db);
    }
    return Json(ToDoItems::get_state(1));
}

从前面的代码中我们可以看到,它就像我们的 Actix Web 实现,因为我们使用的是现有的模块。 唯一的区别是我们将 ToDoItems 结构从 Rocket 箱传递到 Json 函数中。 我们还没有实现身份验证,因此我们现在只是将用户 ID 值 1 传递给所有需要用户 ID 的操作。

现在我们的视图创建完成了,我们可以使用以下代码将其附加到我们的服务器:

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, hello, bye])
                   .mount("/v1/item", routes![item_create])
}

我们可以看到我们不必构建自己的配置函数。 我们可以将与前缀关联的数组中的视图排列起来,装饰视图函数的宏定义 URL 的其余部分。 我们现在可以使用以下命令运行 Rocket 服务器:

cargo run config.yml

我们必须记住启动 docker-compose,以便可以访问数据库并使用diesel 客户端在数据库上运行迁移。 然后,我们可以使用以下 URL 通过发布请求创建第一个待办事项:

http://127.0.0.1:8000/v1/item/create/coding

发出post请求后,我们将得到以下响应正文:

{
    "pending_items": [
        {
            "title": "coding",
            "status": "PENDING"
        }
    ],
    "done_items": [],
    "pending_item_count": 1,
    "done_item_count": 0
}

现在你就拥有了! 我们的应用程序正在运行,我们不需要重新编码整个代码库。 我知道我在整本书中都在重复自己的话,但是结构良好、独立的代码的重要性怎么强调也不为过。 我们在这里所做的在重构系统时很有用。 例如,我曾研究过微服务系统,我们不得不从一台服务器中剥离功能,因为它的范围太大并创建另一台服务器。 正如您在这里所看到的,独立的模块使此类任务成为一个梦想,可以以最少的努力在创纪录的时间内完成。

现在我们已经在基本意义上集成了现有模块,我们可以通过为模块实现 Rocket 特征来继续高级集成。

实施Rocket特征

我们在模块中定义并复制的大部分逻辑都可以在代码中直接引用。 但是,我们确实必须利用数据库连接和具有 Actix Web 特征实现的 JWT 结构。 如果我们要复制视图,则必须为数据库连接和 JWT 身份验证实现 Rocket 特征,因为我们将它们传递到 Actix Web 应用程序中的视图函数中。

在实现 Rocket 特征之前,我们必须使用以下命令复制 JWT 文件:

cp web_app/src/jwt.rs rocket_app/src/jwt.rs

然后,我们必须使用以下代码在 Cargo.toml 文件中声明以下依赖项:

jsonwebtoken = "8.1.0"

现在,我们可以继续使用 src/jwt.rs 文件来实现 Rocket 特征。 首先,我们必须使用以下代码在文件顶部导入以下特征和结构:

use rocket::http::Status;
use rocket::request::{self, Outcome, Request, FromRequest};

FromRequest 实现的核心逻辑是相同的,因为我们关心的是令牌的解码和身份验证。 然而,会有一些细微的差别,因为我们正在实现 Rocket 框架的一个特征,而不是 Actix Web crate。 主要区别在于,我们必须构建自己的枚举,使用以下代码定义可能的结果:

#[derive(Debug)]
pub enum JwTokenError {
    Missing,
    Invalid,
    Expired
}

我们在这里选择了不同的可能性,因为令牌可能不在标头中,因此它会丢失。 或者,该令牌可能不是我们的,因此它可能无效。 请记住,我们有一个时间戳来强制到期时间。 如果令牌已过期,它将处于过期状态。

下一步只是实现 FromRequest 特征。 我们不必触及 JwToken 结构,因为代码是隔离的,只关心令牌的编码和解码。 我们的特征实现的轮廓可以用以下代码定义:

#[rocket::async_trait]
impl<'r> FromRequest<'r> for JwToken {
    type Error = JwTokenError;
    async fn from_request(req: &'r Request<'_>)
                          -> Outcome<Self, Self::Error> {
        . . .
    }
}

在这里,我们可以看到我们用异步特征宏装饰了特征的实现。 这是因为请求以异步方式发生。 我们还必须定义生命周期符号。 这是因为我们必须声明请求的生命周期与特征实现的生命周期相同。 我们可以通过 from_request 函数中的请求参数看到这一点。 现在,我们可以将逻辑从旧的 Actix Web 实现提升到 from_request 函数中,并对返回的类型进行一些更改。 提升后的代码最终应如下所示:

match req.headers().get_one("token") {
    Some(data) => {
        let raw_token = data.to_string();
        let token_result = JwToken::from_token(raw_token);
        match token_result {
            Ok(token) => {
                return Outcome::Success(token)
            },
            Err(message) => {
                if message == "ExpiredSignature".to_owned() {
                    return Outcome::Failure((Status::BadRequest,
                                           JwTokenError::Expired))
                }
                return Outcome::Failure((Status::BadRequest,
                    JwTokenError::Invalid))
            }
        }
    },
    None => {
        return Outcome::Failure((Status::BadRequest,
                                 JwTokenError::Missing))
    }
}

我们可以看到我们已经将回报包含在Rocket结果中,这并不奇怪。 当解码或从标头访问令牌失败时,我们还包含了我们的枚举。

我们的 JwToken 结构现在可以插入我们的 Rocket 应用程序中,但我们必须记住删除旧的 Actix 实现以及对 Actix Web 框架的所有引用。 我们还必须使用以下代码在 main.rs 文件中声明 jwt 模块:

mod jwt;

我们的下一步是为我们的数据库连接实现 FromRequest 特征。 此时,您最好尝试自己实现数据库连接的 FromRequest 特征。 要实现这一目标,您无需了解任何新内容。

如果您尝试自己实现数据库连接的 FromRequest 特征,那么它应该类似于以下步骤。

首先,我们必须使用以下代码在 src/database.rs 文件中导入所需的 Rocket 结构和特征:

use rocket::http::Status;
use rocket::request::{self, Outcome, Request, FromRequest};

然后我们必须定义结果。 我们要么获得连接,要么没有,因此我们的枚举只有一种可能的错误,其形式如下:

#[derive(Debug)]
pub enum DBError {
    Unavailable
}

然后,我们使用以下代码实现数据库连接的 FromRequest 特征:

#[rocket::async_trait]
impl<'r> FromRequest<'r> for DB {
    type Error = DBError;
    async fn from_request(_: &'r Request<'_>)
                          -> Outcome<Self, Self::Error> {
      match DBCONNECTION.db_connection.get() {
         Ok(connection) => {
            return Outcome::Success(DB{connection})
         },
         Err(_) => {
            return Outcome::Failure((Status::BadRequest,
                                     DBError::Unavailable))
         }
      }
    }
}

前面的代码应该不会太令人惊讶; 我们只是将获取数据库连接的现有逻辑与 JwToken 实现中列出的 FromRequest 特征的实现融合在一起。

笔记

您可能已经注意到,我们已经用 [rocket::async_trait] 注释了 FromRequest 实现。 我们使用它是因为,在撰写本文时,Rust 中异步功能的稳定性不包括对特征中异步函数的支持。 如果我们尝试在没有注释的特征中实现异步函数,我们将收到以下错误:

trait fns cannot be declared `async`

[rocket::async_trait] 注释使我们能够在特征实现中定义异步函数。 我们不能简单地对异步函数进行脱糖并具有以下函数签名是有原因的:

async fn from_request(_: &'r Request<'_>)
                      -> Pin<Box<dyn Future<Output
                      = Outcome<Self, Self::Error>>
                      + Send + '_>> {

但是,它不起作用,因为我们无法在特征函数中返回 impl 特征,因为这是不受支持的。 要深入了解为什么特征中的异步函数很难,请访问以下博客文章:https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard /。

现在,我们可以在 main.rs 文件的创建视图中实现数据库连接。 同样,这是您自己尝试使用 FromRequest 特征实现数据库连接的好机会。

如果您尝试在创建视图中使用 Rocket FromRequest 特征,您的代码应如下所示:

#[post("/create/<title>")]
fn item_create(title: String, db: DB) -> Json<ToDoItems> {
    let items = schema::to_do::table
        .filter(schema::to_do::columns::title.eq(&title.as_            str()))
        .order(schema::to_do::columns::id.asc())
        .load::<Item>(&db.connection)
        .unwrap();
    if items.len() == 0 {
        let new_post = NewItem::new(title, 1);
        let _ = diesel::insert_into(schema::to_do::table)
            .values(&new_post)
            .execute(&db.connection);
    }
    return Json(ToDoItems::get_state(1));
}

如果我们再次运行我们的应用程序,然后点击创建端点,我们将看到我们的实现有效! 这表明我们可以将经过一些修改的视图从 Actix Web 应用程序复制并粘贴到我们的 Rocket 应用程序中。 在下一节中,我们将把现有的视图集成到 Rocket Web 应用程序中。

插入我们现有的观点

当涉及到我们的视图时,它们也是隔离的,我们可以将视图复制到 Rocket 应用程序,并进行一些小的更改,以回收我们为 Actix Web 应用程序构建的视图。 我们可以使用以下命令复制视图:

cp -r web_app/src/views rocket_app/src/views

有了这个副本,不言而喻,我们现在必须检查并清除任何提及 Actix Web 框架的视图,因为我们没有使用它。 一旦我们清除了有关 Actix Web 的视图,我们就可以重构现有代码,使其与 Rocket 框架一起工作。 我们将从登录视图开始,因为它接受 JSON 正文并在以下小节中返回 JSON。

接受并返回 JSON

在更改视图之前,我们需要确保已使用以下代码在 src/views/auth/login.rs 文件中导入了所需的所有内容:

use crate::diesel;
use diesel::prelude::*;
use rocket::serde::json::Json;
use crate::database::DB;
use crate::models::user::user::User;
use crate::json_serialization::{login::Login,
                login_response::LoginResponse};
use crate::schema::users;
use crate::jwt::JwToken;

我们可以看到,除了来自 Rocket crate 的 Json 结构之外,没有发生太多变化。 实现这些 Rocket 特征确实帮助我们切断了代码中与 Actix 框架的链接并连接到 Rocket 框架,而无需更改实现这些特征的结构的导入或使用方式。 考虑到这一点,我们的登录视图的以下概要不应令人震惊:

#[post("/login", data = "<credentials>", format = "json")]
pub async fn login<'a>(credentials: Json<Login>, db: DB) ->
                                        Json<LoginResponse> {
    . . .
}

我们可以看到,我们引用传入的 JSON 正文和数据库连接的方式与之前在 Actix 登录视图中所做的方式相同。 主要区别在于突出显示数据是什么以及传入数据采用什么格式的宏。 在登录视图中,我们有以下逻辑:

let username: String = credentials.username.clone();
let password: String = credentials.password.clone();
let users = users::table
    .filter(users::columns::username.eq(username.as_str()))
    .load::<User>(&db.connection).unwrap();
match users[0].clone().verify(password) {
    true => {
        let user_id = users[0].clone().id;
        let token = JwToken::new(user_id);
        let raw_token = token.encode();
        let body = LoginResponse{token: raw_token.clone()};
        return Json(body)
    },
    false => panic!("unauthorised")
}

我们可以在代码中看到,唯一的区别是我们没有返回多个不同的代码,而只是抛出一个错误。 这种方法并不是最佳的。 在以前的版本中,Rocket 框架用于实现简单的响应生成器,例如 Actix。 然而,在撰写本文时,Rocket 在其最近的版本中实施了许多重大更改。 标准响应构建器现在根本无法工作,并且需要复杂的特征实现来返回带有代码、正文和标头中的值的响应。 在撰写本文时,这方面的文档和示例也很有限。 进一步阅读部分提供了有关构建更高级响应的进一步阅读。

现在我们的登录视图已定义,我们可以继续处理返回原始 HTML 的注销视图。

返回原始 HTML

如果您还记得的话,我们的注销机制会返回原始 HTML,该 HTML 在浏览器中运行 JavaScript 以删除我们的令牌。 对于 Rocket,返回原始 HTML 很简单。 在我们的 src/views/auth/logout.rs 文件中,整个代码采用以下形式:

use rocket::response::content::RawHtml;
#[get("/logout")]
pub async fn logout() -> RawHtml<&'static str> {
        return RawHtml("<html>\
                <script>\
                    localStorage.removeItem('user-token'); \
                    window.location.replace(
                        document.location.origin);\
                </script>\
              </html>")
}

我们可以看到它返回一个字符串,就像前面的 Actix Web 视图一样,但该字符串包装在 RawHtml 结构中。 我们现在可以开始更新待办事项操作视图,以便我们的用户可以操作待办事项,如下一节所述。

使用 JSON 返回状态

到目前为止,我们已经返回了 JSON 和原始 HTML。 但是,请记住,我们的待办事项应用程序会返回具有不同状态的 JSON。 为了探索这个概念,我们可以重新访问 src/views/to_do/create.rs 文件中的创建视图,其中我们必须返回带有 JSON 正文的创建状态。 首先,除了来自 Rocket 框架的状态和 JSON 结构之外,我们所有的导入都与之前相同,代码如下:

use rocket::serde::json::Json;
use rocket::response::status::Created;

通过这些导入,我们可以使用以下代码定义创建视图函数的轮廓:

#[post("/create/<title>")]
pub async fn create<'a>(token: JwToken, title: String, db: DB)
                                 -> Created<Json<ToDoItems>> {
    . . .
}

我们可以看到我们的返回值是 Created 结构,其中包含 Json 结构,而 Json 结构又包含 ToDoItems 结构。 我们还可以看到我们的 JWT 身份验证是以相同的方式在视图中实现的,因为我们再次实现了 Rocket 特征。 我们的数据库逻辑与之前的视图相同,如以下代码所示:

let items = to_do::table
    .filter(to_do::columns::title.eq(&title.as_str()))
    .order(to_do::columns::id.asc())
    .load::<Item>(&db.connection)
    .unwrap();
if items.len() == 0 {
    let new_post = NewItem::new(title, token.user_id);
    let _ = diesel::insert_into(to_do::table).values(&new_post)
        .execute(&db.connection);
}

如果数据库中不存在该任务,我们将插入新的待办事项。 完成此操作后,我们将获取系统的状态并使用以下代码返回它:

let body = Json(ToDoItems::get_state(token.user_id));
return Created::new("").body(body)

空字符串是位置。 可以将其留空,不会产生任何后果。 然后我们将我们的身体与状态的身体功能联系起来。 这就是让我们的创建视图按我们想要的方式运行所需的全部。

当谈到我们的待办任务的其他视图时,它们都将是我们为创建视图所做的一些变化。 所有待办事项视图都需要执行以下步骤:

使用 JWT 进行身份验证。

连接到数据库。

从 JSON 正文获取数据和/或从 JWT 获取用户数据。

对数据库中的数据进行一些操作(除了 GET 视图)。

返回用户的数据库状态。

在了解我们对创建视图所做的操作之后,您应该能够处理所有其他视图以使它们与 Rocket 框架兼容。 我们已经涵盖了进行这些更改所需的一切。 在书中详细说明这些更改将导致执行不必要的重复步骤,从而使本书过于臃肿。 这些更改可以在本书的 GitHub 存储库中找到。

一旦执行了待办事项视图,我们就可以继续进行所需的最终视图,即创建用户,其中我们必须根据结果返回不同的状态。

返回多种状态

当创建用户时,我们只返回创建的状态码或冲突状态码,而不返回任何其他内容。 我们不需要返回数据,因为刚刚创建用户的人已经知道用户的详细信息。 在 Rocket 中,我们可以返回多个不同的没有正文的状态代码。 我们可以在 src/views/to_do/create.rs 文件中探索这个概念,但首先,我们必须确保导入以下内容:

use crate::diesel;
use diesel::prelude::*;
use rocket::serde::json::Json;
use rocket::http::Status;
use crate::database::DB;
use crate::json_serialization::new_user::NewUserSchema;
use crate::models::user::new_user::NewUser;
use crate::schema::users;

现在我们已经拥有了所需的一切,我们可以使用以下代码定义视图的轮廓:

#[post("/create", data = "<new_user>", format = "json")]
pub async fn create_user(new_user: Json<NewUserSchema>, db: DB)
    -> Status {
    . . .
}

在这里,我们可以看到,除了返回单个 Status 结构体之外,没有什么新内容。 我们的数据库逻辑采用以下形式:

let name: String = new_user.name.clone();
let email: String = new_user.email.clone();
let password: String = new_user.password.clone();
let new_user = NewUser::new(name, email, password);
let insert_result = diesel::insert_into(users::table)
            .values(&new_user).execute(&db.connection);

我们使用以下代码从两种可能的状态中返回一个状态:

match insert_result {
    Ok(_) => Status::Created,
    Err(_) => Status::Conflict
}

我们的观点是完整的。 现在我们可以继续下一部分,用 Rocket 应用程序注册我们的视图。

向 Rocket 注册我们的观点

在继续处理 src/main.rs 文件之前,我们必须确保 src/main.rs 可以使用我们的视图函数。 这意味着遍历每个视图模块中的所有 mod.rs 文件并将定义这些视图的函数声明为公共。 然后我们可以继续查看 src/main.rs 文件并确保导入以下内容:

#[macro_use] extern crate rocket;
#[macro_use] extern crate diesel;
use rocket::http::Header;
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};

Macro_use 声明应该不足为奇; 但是,我们导入 Rocket 结构来定义我们的 CORS 策略。 导入这些包后,我们现在必须确保已声明以下模块:

mod schema;
mod database;
mod json_serialization;
mod models;
mod to_do;
mod config;
mod jwt;
mod views;

您应该对这些模块很熟悉。 然后我们必须使用以下代码导入我们的视图:

use views::auth::{login::login, logout::logout};
use views::to_do::{create::create, delete::delete,
                   edit::edit, get::get};
use views::users::create::create_user;

现在我们已经进口了所需的一切。 在服务器上声明我们的视图之前,我们需要定义我们的 CORS 策略。 这是通过声明一个没有字段的结构来实现的。 然后,我们为该结构实现 Fairing 特征,以允许流量。 整流罩本质上定义了中间件。 有关整流罩的更多信息请参阅进一步阅读部分。 我们的 CORS 策略可以使用以下代码定义:

pub struct CORS;
#[rocket::async_trait]
impl Fairing for CORS {
    fn info(&self) -> Info {
        Info {
            name: "Add CORS headers to responses",
            kind: Kind::Response
        }
    }
    async fn on_response<'r>(&self, _request: &'r Request<'_>,
                                response: &mut Response<'r>) {
        response.set_header(Header::new(
                         "Access-Control-Allow-Origin", "*"));
        response.set_header(Header::new(
                        "Access-Control-Allow-Methods",
                        "POST, GET, PATCH, OPTIONS"));
        response.set_header(Header::new(
                        "Access-Control-Allow-Headers", "*"));
        response.set_header(Header::new(
                        "Access-Control-Allow-Credentials",
                        "true"));
    }
}

到目前为止,我们已经熟悉了 CORS 的概念以及如何实现 Rocket 特征。 前面的代码无需赘述。

现在我们已经拥有将视图安装到服务器所需的一切,代码如下:

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index, hello, bye])
                   .mount("/v1/item/", routes![create, delete,
                                               edit, get])
                   .mount("/v1/auth/", routes![login, logout])
                   .mount("/v1/user/", routes![create_user])
                   .attach(CORS)
                   .manage(CORS)
}

再次强调,无需解释。 您可能已经注意到,我们已经开始简单地显示代码,几乎没有解释。 这很好,因为我们已经熟悉了我们正在使用的构建块。 别担心——我们已经完成了主 Rocket 应用程序的构建,因为它将运行并完成我们需要的一切。 我们可以手动测试这一点。 然而,这需要时间并且容易出错。

请记住,我们使用 Postman 在 Newman 中构建了测试! 在下一节中,我们将使用现有的测试管道通过一些命令来测试所有端点。

插入我们现有的测试

因为我们在测试管道中使用了 Newman,所以我们不必担心与我们选择的 Web 框架的高耦合性。 首先,我们需要使用以下命令复制脚本目录中的测试:

cp -r web_app/scripts rocket_app/scripts

但是,在运行之前,我们必须为登录视图添加一个 GET 方法,大纲如下:

#[get("/login", data = "<credentials>", format = "json")]
pub async fn login_get<'a>(credentials: Json<Login>, db: DB)
                                    -> Json<LoginResponse> {
    // same logic as in the login view
}

然后,我们需要将此视图导入到 src/main.rs 文件中,并在服务器的身份验证安装中声明它。 我们现在准备使用以下命令运行完整测试:

sh scripts/run_test_pipeline.sh

这将运行我们的完整管道并给出以下结果:


我们可以看到,在 64 项检查中,只有 3 项失败。 如果我们进一步向下滚动,我们可以看到错误的发生只是因为我们为创建视图返回了不同的响应代码,如下所示:

  #  failure                   detail
 1.  AssertionError            response is ok
                               expected response to have status
                               code 200 but got 201
                               at assertion:0 in test-script
                               inside "1_create"
 2.  AssertionError            response is ok
                               expected response to have status
                               code 200 but got 201
                               at assertion:0 in test-script
                               inside "2_create"
 3.  AssertionError            response is ok
                               expected response to have status
                               code 200 but got 201
                               at assertion:0 in test-script
                               inside "3_create"

其他一切,就登录、身份验证、迁移以及每个步骤之间数据库中的数据状态而言,都表现得像我们预期的那样。

概括

在本章中,我们已经了解了复制待办事项应用程序所需的主要概念。 我们构建并运行了一个 Rocket 服务器。 然后我们定义路由并为我们的服务器建立数据库连接。 之后,我们探索了中间件并构建了身份验证和数据处理,并为我们的视图使用了守卫。 至此,我们创建了一个利用本书中介绍的所有内容的视图。

我们在这里获得的是对我们在本书中构建的模块化代码的更深入的了解。 尽管自本书开始以来,我们重新审视的一些概念尚未触及,但这些模块是孤立的,只做一件事,并且按照其标签的建议进行操作。 正因为如此,它们可以很容易地被复制并在完全不同的框架中使用。 我们的测试管道也派上了用场,立即确认我们的 Rocket 应用程序的行为与我们的 Actix Web 应用程序的行为相同。 考虑到这一点,我们的 Rocket 应用程序可以无缝集成到我们的构建和部署管道中,而不是我们的 Actix Web 应用程序。

在下一章中,我们将介绍构建 Web 应用程序的最佳实践,从而创建一个干净的 Web 应用程序存储库。 在这里,您不仅将学习如何在测试和配置方面构建 Web 应用程序存储库,还将学习如何在 Docker 中将 Web 应用程序打包为无发行版,从而生成大约 50 MB 的小型 Docker 映像。

最近发表
标签列表