Rocket使用小结

renyuneyun 2018年08月03日(周五) 2 mins

在今年Increase Rust's Reach中,我参与Rust新网站的i18n及l10n。其中新网站要基于 Rocket 构建,所以也就(跟着 官方教程 )学习了一下Rocket。 既然学了,就顺便记录一点心得和体会,以方便后来者。

Rocket是一个 web框架 。我个人对web编程(尤前端)并不太感兴趣(主要是感到 web技术栈 太过麻烦/复杂),所以涉及不太多,之前也只用过Python那边的Flask以及(一小段时间)Django以及Go自带的http服务器,故而本文不怎么会涉及和其他web框架的对比。

本文不打算成为通常意义上的Rocket教程,而只是打算给有兴趣者一个快速的(对rocket的)观感。其中也会有一些个人的经验教训等。

Rocket概览

类似我之前用过的框架,Rocket也将函数作为不同的路由的处理器。Rocket在每个函数之前使用形如 #[get("/myroute")]属性 作为标记,之后在Rocket入口对象/结构体上对所需要的路由(函数)进行 mount 即可。

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

fn main() {
    rocket::ignite()
        .mount("/", routes![index])
        .launch();
}

(代码片段来自 getting-started

这点粗看很像Flask中使用 @app.route() 进行路由设定,仅有这两点不同:

  • Rocket不使用全局的app对象
  • 路由可以定义但不挂载

我最初也以为Rocket和Flask的设计极为相似,且两者都只打算做web框架而不涉及其他;然而,越到后面越是发现两者不同之处的巨大。相对而言,我个人更喜欢Rocket的设计:更加函数导向(亦强调使用局部变量)。

  • 在Rocket中,函数是处理路由的全部,不需要使用如flask中魔法一般的全局 request 对象;
  • 函数的参数和属性中的设定共同决定了路由是否匹配,手动类型的优势在这里有所体现;
  • 各种(预定义或自定义的) 请求哨卫 可以被添加到函数参数表中参与决定路由的匹配性;
  • 使用 整流器 在请求到达前或应答发送时对请求或应答进行调整;
  • 使用 状态 做状态存储,以便的确需要“全局”变量的情况。

路由匹配

Rocket通过在属性上设定不同的HTTP方法以及URL段来做匹配。 具体细节见 文档#methods ,这里仅做摘要:

  • 常见HTTP方法均被支持,只是每次只能设定一个方法
  • 在未定义时,HEAD请求会被自动转到相应的GET请求上(不过会删除应答体)
  • 表单首个字段为 _method 时,POST请求会被自动重译为相应的请求
    • 该设定是为了方便浏览器,毕竟浏览器通常只有GET和POST

在路由的URL上,可以设定将部分(或全部) 注入到函数的对应参数中。Rocket会自动进行类型转换,且仅匹配转换成功的路由。

#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: String, age: u8, cool: bool) -> String {
    if cool {
        format!("You're a cool {} year old, {}!", age, name)
    } else {
        format!("{}, we need to talk about your coolness.", name)
    }
}

(代码片段来自 文档#methods

自定义类型也可用在路由匹配中,只要其实现了 FromParam trait即可。

请求及应答

在不考虑整流器的情况下,用户的请求将直接进入相应的路由函数中,然后经过函数的处理,最后函数的返回值将作为应答。路由匹配的过程即是请求处理器的选择过程。

Rocket不要求路由函数的返回值是一个HTTP应答 Response ,而是通过 Responder 机制方便编程:路由函数的返回值需要是一个实现了 Responder trait的类型,而Rocket负责调用 Responder 的相关函数将路由函数的返回值转换为HTTP应答。这样,在Rocket中我们便可以用 String 等类型作为函数返回值。

Rocket提供一些实现以应对常见的应答情况:

  • 应答包装器 可以包含其他 Responder ,并且执行自己的修改
    • status 可以覆盖应答中的状态代码
    • content 可以覆盖应答中的 Content-Type 字段
    • Error (我没尝试过,所以直接链接至官方文档)
  • String&str 会被作为应答体,且 Content-Type 会被设置为 text/plain
  • Option 是应答包装器, :rust`Option<T>` 的 T 需要实现 Responder
    • 当是 Some 时,其内容将会被作为应答
    • 当是 None 时,返回404
  • Result 是应答包装器,且其功能取决于 E 是否实现 Responder
    • E 实现了 Responder ,则该 Result 会被作为应答(无论是 Ok 还是 Err
    • E 没有实现 Responder ,则 Ok 会被作为应答,但 Err 会被记录在终端中且返回500

文档#rocker-responder 中还列出了几个常见的对于HTTP很有意义的 Responder 实现,包括下面所说的 Template 。)

网页模板

作为一个web框架,提供对网页模板的支持几乎是理所应当。Rocket本身提供了 Template 机制,而在 rocket_contrib crate中提供了一些特定模板支持。

Template 被实现为一个 Responder ,这样让响应函数返回 Template 类型即可:

#[get("/")]
fn index() -> Template {
    let context = /* object-like value */;
    Template::render("index", &context)
}

Rocket不限制使用何种模板,但官方文档提到了 .hbs Handlebars和 .tera Tera。而Rocket的 Template 机制之所以有效,还需要 整流器 的帮助——所以需要在Rocket实例上 .attach(Template::fairing()); 以便可以正确使用模板。

整流器

依Rocket文档所说,整流器的功能类似于中间件,可以介入请求和应答过程以进行额外操作。由于我没有学过相关课程,也没有太多了解相关知识,所以无法给出个人对此的看法,只能照搬官方文档的说法。

在类似其他框架的中间件之外,Rocket对整流器的功能有一些额外规定:

  • 整流器 不能 终止或直接响应请求
  • 整流器 不能 任意注入非请求数据至请求中
  • 整流器可以阻止程序的启动
  • 整流器可以修改程序的配置

文档中fairings部分 对整流器有更多说明,但对我来说最需要知道的还有这些:

  • 整流器应当只用于“全局”适用的东西(比如做日志)
  • 更多时候,需要的其实是 请求哨卫数据哨卫
  • 整流器按顺序执行,所以`.attach()`的顺序需要注意

全局共享数据

这里的“全局”指的是Rocket之内,在各个路由中共享数据。由于路由是由Rocket管理的,故而其参数表中没办法添加更多参数;而Rust又没有全局变量(即使有也不符合美感),故而Rocket提供的 状态 机制可谓实用非常。 官方教程state部分 <https://rocket.rs/guide/state/> 中也教导使用 状态 来管理数据库连接。

使用上, 状态 同样通过 请求哨卫 机制,作为路由函数的参数。任何类型的数据均可作为 状态 ,且不需要额外实现任何东西。唯一的要求就是在Rocket实例载入时要求 管理状态

像这样要求Rocket去 管理 某个 状态

struct HitCount {
    count: AtomicUsize
}

rocket::ignite()
    .manage(HitCount { count: AtomicUsize::new(0) });

像这样要求在某路由函数上使用某 状态

#[get("/count")]
fn count(hit_count: State<HitCount>) -> String {
    let current_count = hit_count.count.load(Ordering::Relaxed);
    format!("Number of visits: {}", current_count)
}

#[get("/state")]
fn state(hit_count: State<HitCount>, config: State<Config>) -> T { ... }

需要注意的是,Rocket对每种 数据类型 管理一个 状态 ,而 不是 每个数据。

另外,在自定义的 请求哨卫 中,也可以使用 状态

fn from_request(req: &'a Request<'r>) -> request::Outcome<T, ()> {
    let hit_count_state = req.guard::<State<HitCount>>()?;
    let current_count = hit_count_state.count.load(Ordering::Relaxed);
    ...
}

(代码片段来自 文档的state部分

总结

总得来说,我个人对Rocket的设计较为欣赏/膜拜,尤其是其对Rust各项机制的有效利用。

之前用其他框架时总有觉得别扭的地方,但它们均不存在于Rocket中,让我写起来觉得比较顺手:

  • Django(2013年底或2014年初)
    • 框架内耦合性太强,但框架的手又过长
    • 什么都想让框架承包,初学者用起来束手束脚
      • 当然,也可以说是我还没有体会到Django的好处。但我实在是对封闭花园式的东西感到反感,所以不见得可以体会到Django的妙处
    • 而且当年对Py3的支持还不怎么样,但我又恰恰想用Py3
  • Go(2015年)
    • Go的自带http库直接将请求和应答对象作为参数传入,手动解析很难受
    • Go的html模板是语言提供的,灵活度上让我怀疑
    • 当时(2015年)查过其他的go语言web框架,比较看好的有beego以及另一个想不起来名称的。但其教程写得并不如人意(不如我意),又由于当年需求十分简单,所以直接裸上语言库
  • Flask(2016年)
    • Flask要使用全局的app对象和db对象,设计上很诡异
    • Flask要使用魔法一般的request对象,总让人觉得不安心
      • 且request对象暴露太多内容,类似Go用http库的感觉(和使用Android的Context对象的感觉很像)

然而,我对Rocket的部分trait和/或类型设计仍有疑惑,还在寻找解决方案的过程中。 另外,我暂时还没有在Rust中使用过数据库连接,所以无法对其联合使用后的手感做出评价。 但综合来看,Rocket的设计可以说是我所用过的所有框架中最符合我心意的框架;而且它的设计理念可以说符合了我对web框架所构想的所有主要要求。如果不是因为Rust仍算小众,Rocket的用户量和教程量应当早就超过现有的数量了吧(2018-08-02 Google搜“Rust Rocket 教程”,得到11,200条结果)。


Related posts:

您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。