Sapper 是一个轻量的 Web 框架,目的是为了方便使用。做个类比吧,这个框架的量级类似于 Python 的 Falcon,但会比 Falcon 的完全裸装更高一些,接近于 Flask。
Sapper 主要依赖于 Hyper 0.10.13 提供 http server,当异步能够使用的时候(Rust 标准库完成),会考虑改成异步模式。
Sapper 框架分为几个部分组成:
- Sapper:主库,export 几个重要的 trait 和 struct,主要负责路由解析、错误处理等基本的框架功能,提供一个小的静态文件服务器的功能,单一的 Sapper 库是可以作为框架使用的,只是少一些比较方便的功能,所有流式数据需要自己转成想要的格式;
- Sapper_std:对一些脚手架功能的包装,合并为 std 库,供用户使用,提供大量方便可用的 macro(宏),同时将网络请求数据中的 Query/Form/Jsonbody 数据解析成方便使用的格式,可以很轻松的使用。
一般来说,在项目中,只需要引用上面两个库就可以正常工作了,当然,如果想要自己解析原始数据,也可以只引用 Sapper 主库。
下面几个库的功能全部被 Sapper_std 库引用并 export。
-
Sapper_session 提供识别并解析 cookies 的 function,以及设置 cookies 的 function。
-
Sapper_logger 提供一个简单的 log 输出能力,日志格式如下:
[2017-12-09 12:20:12] GET /ip/view Some("limit=25&offset=0") -> 200 OK (0.700839 ms)
-
Sapper_tmpl 引入 tera 这个 Jinja2 模板库,使 Sapper 能后端渲染 web 页面
-
Sapper_query 解析 url 查询字符
-
Sapper_body 解析请求 body 的数据: Form/Json
Sapper 最大的特点是把 web 业务分为了三个层次(全局、模块、处理函数)去处理,每个模块都可以有自己的路由和中间件,全局可以定义全局共享的中间件和全局变量。把颗粒度放到最细就意味着每一个请求都可以有自己的中间件。
这个示例尽量把 Sapper 的所有功能写进去,不过依然会有缺漏,一些没有写到的地方,可以参考 Sapper_example
$ cargo new sapper_demo
在 cargo.toml 文件中增加依赖,为了演示自定义全局 not found 页面的功能(还未向 crates.io publish),依赖上增加 [patch] 项:
[dependencies]
sapper = "^0.1"
sapper_std = "^0.1"
serde="*"
serde_json="*"
serde_derive="*"
[patch.crates-io]
sapper = { git = 'https://github.com/sappworks/sapper.git' }
一个完整的 Sapper 项目目录大概如下,static 目录下的是静态文件,比如 js/css/png,views 目录下的是 web 模板文件,这个地方固定得比较死,tera 在 Sapper 内部指定加载这个目录下的所有文件。
|-- src
| |-- bin
| | |-- main.rs
| |-- lib.rs
| |-- bar.rs
| |-- foo.rs
|-- static
|-- views
|-- Cargo.lock
|-- Cargo.toml
在 lib.rs
下加入以下代码:
extern crate sapper;
#[macro_use]
extern crate sapper_std;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
pub mod foo;
pub mod bar;
pub use foo::Foo;
pub use bar::{ Bar, Global };
接下来先写好 main.rs
启动项:
extern crate sapper;
extern crate sapper_std;
extern crate sapper_demo;
use sapper::{ SapperApp, SapperAppShell, Request, Response, Result as SapperResult };
use sapper_demo::{ Foo, Bar, Global };
use std::sync::Arc;
struct WebApp;
impl SapperAppShell for WebApp {
fn before(&self, req: &mut Request) -> SapperResult<()> {
sapper_std::init(req, Some("session"))?;
Ok(())
}
fn after(&self, req: &Request, res: &mut Response) -> SapperResult<()> {
sapper_std::finish(req, res)?;
Ok(())
}
}
fn main() {
let global = Arc::new(String::from("global variable"));
let mut app = SapperApp::new();
app.address("127.0.0.1")
.port(8080)
.init_global(
Box::new(move |req: &mut Request| {
req.ext_mut().insert::<Global>(global.clone());
Ok(())
})
)
.with_shell(Box::new(WebApp))
.add_module(Box::new(Foo))
.add_module(Box::new(Bar))
.static_service(true)
.not_found_page(String::from("not found"));
println!("Start listen on {}", "127.0.0.1:8080");
app.run_http();
}
上面的代码目前是不能运行的,Foo
和 Bar
这两个结构体还没有定义。这里首先讲解一下上面代码:
SapperApp
是 Sapper 库中的核心结构之一,整个 web 项目都围绕这个结构进行,下面介绍一下它的一些常用方法(具体函数签名可以直接看源码):
fn init_global()
将想要全局共享的变量(如数据库连接池)注册在应用中。
fn with_shell()
注册全局中间件,上面代码将 std 的 fn init()
和 fn finish()
方法写入全局中间件。 init
将请求的各种参数解析并写入 SapperRequest
,同时初始化 log,写入关注的 session key 值, finish
输出 log。
fn add_moudle()
注册子模块,每个模块需要符合 SapperMoudle
trait。
fn static_server()
默认为 true
, 即开启静态文件服务器功能。
fn not_found_page()
默认为 None,即如果路由没有指定的话,会返回 "404 not found" 字符串,如果需求自定义 404 页面,直接将对应的字符串传入即可。
在 foo.rs 里加上代码:
use sapper::{ SapperModule, SapperRouter, Response, Request, Result as SapperResult };
use sapper_std::{ QueryParams, PathParams, FormParams, JsonParams, Context, render };
use serde_json;
pub struct Foo;
impl Foo {
fn index(_req: &mut Request) -> SapperResult<Response> {
let mut web = Context::new();
web.add("data", &"Foo 模块");
res_html!("index.html", web)
}
// 解析 `/query?query=1`
fn query(req: &mut Request) -> SapperResult<Response> {
let params = get_query_params!(req);
let query = t_param_parse!(params, "query", i64);
let mut web = Context::new();
web.add("data", &query);
res_html!("index.html", web)
}
// 解析 `/user/:id`
fn get_user(req: &mut Request) -> SapperResult<Response> {
let params = get_path_params!(req);
let id = t_param!(params, "id").clone();
println!("{}", id);
let json2ret = json!({
"id": id
});
res_json!(json2ret)
}
// 解析 body json 数据
fn post_json(req: &mut Request) -> SapperResult<Response> {
#[derive(Serialize, Deserialize, Debug)]
struct Person {
foo: String,
bar: String,
num: i32,
}
let person: Person = get_json_params!(req);
println!("{:#?}", person);
let json2ret = json!({
"status": true
});
res_json!(json2ret)
}
// 解析 form 数据
fn test_post(req: &mut Request) -> SapperResult<Response> {
let params = get_form_params!(req);
let foo = t_param!(params, "foo");
let bar = t_param!(params, "bar");
let num = t_param_parse!(params, "num", i32);
println!("{}, {}, {}", foo, bar, num);
let json2ret = json!({
"status": true
});
res_json!(json2ret)
}
}
impl SapperModule for Foo {
fn before(&self, _req: &mut Request) -> SapperResult<()> {
Ok(())
}
fn after(&self, _req: &Request, _res: &mut Response) -> SapperResult<()> {
Ok(())
}
fn router(&self, router: &mut SapperRouter) -> SapperResult<()> {
router.get("/foo", Foo::index);
router.get("/query", Foo::query);
router.get("/user/:id", Foo::get_user);
router.post("/test_post", Foo::test_post);
router.post("/post_json", Foo::post_json);
Ok(())
}
}
bar.rs
代码:
use sapper::{ SapperModule, SapperRouter, Response, Request, Result as SapperResult, Key, Error as SapperError };
use std::sync::Arc;
use sapper::header::ContentType;
pub struct Bar;
impl Bar {
fn index(_req: &mut Request) -> SapperResult<Response> {
let mut res = Response::new();
res.headers_mut().set(ContentType::html());
res.write_body(String::from("bar"));
Ok(res)
}
}
impl SapperModule for Bar {
fn before(&self, req: &mut Request) -> SapperResult<()> {
let global = req.ext().get::<Global>().unwrap().to_string();
let res = json!({
"error": global
});
Err(SapperError::CustomJson(res.to_string()))
}
fn after(&self, _req: &Request, _res: &mut Response) -> SapperResult<()> {
Ok(())
}
fn router(&self, router: &mut SapperRouter) -> SapperResult<()> {
router.get("/bar", Bar::index);
Ok(())
}
}
pub struct Global;
impl Key for Global {
type Value = Arc<String>;
}
views
文件夹下新增文件 index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="/foo.css" rel="stylesheet"/>
<title>index</title>
</head>
<body>
<p>{{ data }}</p>
</body>
</html>
static
文件夹下新增文件 foo.css
:
p {
color: red;
text-align: center;
}
现在,demo 项目可以通过 cargo run
命令启动,监听在 127.0.0.1:8080
。可以通过 curl
或者 Python 库 httpie 的命令 http
进行测试。
Foo
模块总共 5 个路由,分别展示 web 渲染、Query 解析、Path 解析、json 数据解析、Form 数据解析。除了标定的路由,其他请求都会到 Error::NotFound
这个错误处理里面,即返回 SapperApp
设定的 not found 界面,日志中不体现,设置了路由的访问,可以通过日志看到访问信息。
Bar
模块设置了一个中间件,直接返回错误,这种情况下,访问 127.0.0.1:8080/bar
会直接在中间件被返回,即得到那个全局变量 global
。
https://github.com/sappworks/sapper_examples/tree/master/sapper_demo
Sapper 的中间件和 Error 处理密切相关,下面是两个 trait 和 SapperResult 的源码:
type Result<T> = std::result::Result<T, Error>;
trait SapperModule: Sync + Send {
fn before(&self, req: &mut SapperRequest) -> Result<()> {
Ok(())
}
fn after(&self, req: &SapperRequest, res: &mut SapperResponse) -> Result<()> {
Ok(())
}
fn router(&self, &mut SapperRouter) -> Result<()>;
}
trait SapperAppShell {
fn before(&self, &mut SapperRequest) -> Result<()>;
fn after(&self, &SapperRequest, &mut SapperResponse) -> Result<()>;
}
SapperModule 默认实现了 before 和 after。
整个 Sapper 中间件的运行机制是 请求 -> 全局 before -> 模块 before -> 模块对应路由的处理函数 -> 模块 after -> 全局 after -> 返回,在这期间,如果出现 Error 那么就直接跳出正常后续操作,原路返回。例如一个请求被模块 before 挡住了,那么就直接跳出,根据 Error 类型向请求方返回数据。
Sapper 的中间件正常返回值都是 Ok(())
,意思是继续下去,如果返回 Err(Error)
就是直接跳出,根据 Error 类型进行处理,Error 类型如下:
pub enum Error {
InvalidConfig,
InvalidRouterConfig,
FileNotExist,
NotFound,
Break, // 400
Unauthorized, // 401
Forbidden, // 403
TemporaryRedirect(String), // 307
Custom(String),
CustomHtml(String),
CustomJson(String),
}
Error 中带 String 的类型以及 NotFound 都是可以自定义返回值的
,比如 CustomHtml
和 CustomJson
,可以根据自己需求向其中填入对应的字符串,错误处理将根据类型不同返回不同的 head 信息。
其他的类型返回的是固定的字符串,具体可以看源码 app.rs
第 196 行。
Sapper 源码中,SapperRequest 是对 hyper request 的魔改封装:
pub struct SapperRequest<'a, 'b: 'a> {
raw_req: Box<HyperRequest<'a, 'b>>,
ext: TypeMap
}
这个 typemap::TypeMap 就是核心了,是一个安全的类型值存储 Map。Sapper 中的 Query,Form,Cookies,Json 等信息都存储在这个地方,有兴趣的话,可以看看源码。
无耻地放出了一个博客源码地址: https://github.com/driftluo/MyBlog
欢迎加入 Sapper 社区,欢迎提供高质量的代码,高质量的思路。