在今年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提供一些实现以应对常见的应答情况:
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条结果)。
您可以在Hypothesis上的該群組內進行評論,或使用下面的Disqus評論。