Skip to content

Latest commit

 

History

History
390 lines (304 loc) · 13.1 KB

Tutorial-cn.md

File metadata and controls

390 lines (304 loc) · 13.1 KB

入门教程

前言

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 最大的特点是把 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 };

bin

接下来先写好 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();
}

上面的代码目前是不能运行的,FooBar 这两个结构体还没有定义。这里首先讲解一下上面代码:

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 and Bar

在 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 模块

Foo 模块总共 5 个路由,分别展示 web 渲染、Query 解析、Path 解析、json 数据解析、Form 数据解析。除了标定的路由,其他请求都会到 Error::NotFound 这个错误处理里面,即返回 SapperApp 设定的 not found 界面,日志中不体现,设置了路由的访问,可以通过日志看到访问信息。

Bar 模块

Bar 模块设置了一个中间件,直接返回错误,这种情况下,访问 127.0.0.1:8080/bar 会直接在中间件被返回,即得到那个全局变量 global

demo 源码

https://github.com/sappworks/sapper_examples/tree/master/sapper_demo

中间件和 Error 处理

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 都是可以自定义返回值的 ,比如 CustomHtmlCustomJson,可以根据自己需求向其中填入对应的字符串,错误处理将根据类型不同返回不同的 head 信息。

其他的类型返回的是固定的字符串,具体可以看源码 app.rs196 行。

Sapper 源码

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

Contribute

欢迎加入 Sapper 社区,欢迎提供高质量的代码,高质量的思路。