在這個章節中,會繼續介紹rocket_contrib這個Rocket官方的擴充套件所提供的有關HTTP回應的功能,包括JSON和MessagePack格式的回應,以及Handlebars和Tera這兩個HTML模板引擎。此外還會介紹Rocket框架本身提供的「快閃訊息」(Flash Message)機制。



JSON

在之前的章節中有提到rocket_contribjson模組提供的Json結構體可以作為路由處理程序的函式的回傳值型別,所以該怎麼樣才能建立一個Json結構實體呢?

我們可以替自己實作出來的型別加上Serialize這個derive屬性的參數,使這個型別的資料可以直接被Rocket框架序列化。使用rocket_contrib::json::Json結構體來包裹可被Rocket框架序列化的資料型別,就可以將包裹的資料序列化成JSON格式的字串了!

例如:

#[macro_use]
extern crate serde_derive;
extern crate rocket_contrib;
    
use rocket_contrib::json::Json;

#[derive(Serialize)]
struct User {
    id: i32,
    name: String,
}

#[get("/david")]
fn handler() -> Json<User> {
    Json(User {
        id: 5,
        name: "David".to_string()
    })
}

這邊要注意的地方是,Serialize這個derive屬性的參數來自於serde_derive這個crate。在啟用rocket_contribjson特色時,記得也要將serdeserde_derive這兩個crate給引用至程式專案中。

當路由處理程序的函數是回傳rocket_contrib::json::Json時,HTTP回應中的標頭欄位除了之前提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為application/json

MessagePack

在之前的章節中有提到rocket_contribmsgpack模組提供的MsgPack結構體可以作為路由處理程序的函式的回傳值型別,所以該怎麼樣才能建立一個MsgPack結構實體呢?

我們可以替自己實作出來的型別加上Serialize這個derive屬性的參數,使這個型別的資料可以直接被Rocket框架序列化。使用rocket_contrib::msgpack::MsgPack結構體來包裹可被Rocket框架序列化的資料型別,就可以將包裹的資料序列化成MessagePack格式的資料了!

例如:

#[macro_use]
extern crate serde_derive;
extern crate rocket_contrib;
    
use rocket_contrib::msgpack::MsgPack;

#[derive(Serialize)]
struct User {
    id: i32,
    name: String,
}

#[get("/david")]
fn handler() -> MsgPack<User> {
    MsgPack(User {
        id: 5,
        name: "David".to_string()
    })
}

這邊要注意的地方是,Serialize這個derive屬性的參數來自於serde_derive這個crate。在啟用rocket_contribmsgpack特色時,記得也要將serdeserde_derive這兩個crate給引用至程式專案中。

當路由處理程序的函數是回傳rocket_contrib::msgpack::MsgPack時,HTTP回應中的標頭欄位除了之前提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為application/msgpack

HTML模板引擎

rocket_contrib可以使Rocket框架支援Handlebars和Tera這兩種HTML模板引擎。前者被廣泛用於JavaScript生態圈;後者則是參考了在Python生態圈中被廣泛使用的Jinja2,再運用Rust程式語言所重新開發出來的HTML模板引擎。

Handlebars的官方網站:

Tera的官方網站:

雖然Tera的效能會比Handlebars還要好很多,不過會用Tera的前端工程師實在不多呀!執行效能固然重要,但也要考慮到開發及後續維護的難易度,所以在權衡之下,很多時候我們還是得使用Handlebars模板。

透過rocket_contrib來套用Handlebars和Tera的模板時,只需要使用一種抽象的rocket_contrib::templates::Template結構體來操作即可。

若要使rocket_contrib::templates::Template結構體支援Handlebars,必須要在Cargo程式專案的設定檔中啟用rocket_contribhandlebars_templates特色。如下:

[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["handlebars_templates"]

若要使rocket_contrib::templates::Template結構體支援Tera,必須要在Cargo程式專案的設定檔中啟用rocket_contribtera_templates特色。如下:

[dependencies.rocket_contrib]
version = "*"
default-features = false
features = ["tera_templates"]

模板引擎的整流片(Fairing)

要在Rocket中使用rocket_contrib::templates::Template結構體,需要先使用rocket_contrib::templates::Template結構體的fairing關聯函數來取得必要的整流片,並註冊給Rocket來使用。

程式如下:

extern crate rocket_contrib;

use rocket_contrib::templates::Template;

...

let rocket = rocket::ignite();

let rocket = rocket.attach(Template::fairing());

...

這個整流片會在Attatch事件發生時,去解析Rocket應用程式根目錄底下的templates目錄中的HTML模板檔案。Handlebars的模板檔案使用的副檔名為.hbs,Tera的模板檔案使用的副檔名為.tera,如果同時使用相同檔案名稱(例如:index.hbsindex.tera)的Handlebars和Tera模板檔案,在這個解析的過程中就會被檢查出來,而使Rocket出現警告訊息。

在不使用Release模式來編譯程式專案的情況下,這個整流片還會去監聽Request事件,在每次接收到HTTP請求時都會去檢查templates目錄中的HTML模板檔案有無改變,如果有的話就重新解析。如此一來,在開發HTML模板時,即便不重啟Rocket應用程式也可以直接套用新的模板變更,方便開發者直接在網頁瀏覽器上刷新頁面。

如果需要針對模板引擎來進行設定的話,在取得整流片時,應使用rocket_contrib::templates::Template結構體的custom關聯函數來客製出自己的整流片。

例如以下程式,可以讓Handlebars模板能夠使用我們自己製作好的幫手(Helper)。

#[macro_use]
extern crate handlebars;

extern crate rocket_contrib;

use rocket_contrib::templates::Template;

...

let rocket = rocket::ignite();

let rocket = rocket.attach(custom(|engines| {
        handlebars_helper!(inc: |x: i64| x + 1);
        handlebars_helper!(dec: |x: i64| x - 1);
        handlebars_helper!(eq_str: |x: str, y: str| x == y);
        handlebars_helper!(ne_str: |x: str, y: str| x != y);

        engines.handlebars.register_helper("inc", Box::new(inc));
        engines.handlebars.register_helper("dec", Box::new(dec));
        engines.handlebars.register_helper("eq_str", Box::new(eq_str));
        engines.handlebars.register_helper("ne_str", Box::new(ne_str));
    }));

...

注意這邊的handlebars_helper巨集是由handlebars這個crate提供的哦!記得也要將它給引用至程式專案中。

用模板引擎來產生HTML類型的HTTP回應

要將rocket_contrib::templates::Template結構體作為路由處理程式函數的回傳值型別,程式寫法如下:

extern crate rocket_contrib;

use std::collections::HashMap;

use rocket_contrib::templates::Template;

#[get("/")]
fn index() -> Template {
    let context: HashMap<String, String> = HashMap::new();

    Template::render("index", &context)
}

利用rocket_contrib::templates::Template結構體提供的render關聯函數,我們可以指定一個位於Rocket應用程式根目錄底下的templates目錄中的HTML模板檔案的檔案相對路徑(不含副檔名),以及要用來進行模板填空或是流程控制的「上下文」(context)。這個「上下文」必須要可以被Rocket序列化(換句話說就是必須實作serde框架的Serialize特性)。

當路由處理程序的函數是回傳rocket_contrib::templates::Template時,HTTP回應中的標頭欄位除了之前提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會被設定為text/html; charset=utf-8

快閃訊息(Flash Message)

快閃訊息是Rocket框架提供的一種利用Cookie來進行一次性訊息傳遞的機制。什麼時候我們會用到「一次性訊息」呢?例如會員的網頁後台系統,在會員登入前,只能讓他們看到登入表單而無法進入會員的網頁後台,登入表單必須要填入正確的帳號密碼才可以登入,如果登入失敗,就會顯示錯誤訊息;如果登入成功,就在會員後台顯示歡迎訊息。上述所提到的「顯示錯誤訊息」和「顯示歡迎訊息」,都屬於「一次性訊息」,需要在重新整理頁面後使訊息消失。

當我們想透過後端來傳送一次性訊息給前端時,就可以利用Rocket的快閃訊息機制,先將要顯示的一次性訊息從伺服器端設定進客戶端的Cookie中,當客戶端取得某個特定的網頁時,伺服器端就會去查看客戶端傳來的Cookie中是否有預期會有的一次性訊息,如果有的話就讀取出來,並順便把該Cookie記錄刪除掉。

例如以下程式,簡單模擬出了擁有快閃訊息機制的登入頁面和後台頁面:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

extern crate rocket_contrib;

use std::collections::HashMap;

use rocket::http::{Cookies, Cookie};

use rocket::request::{Form, FlashMessage};

use rocket::response::{Flash, Redirect};

use rocket_contrib::templates::Template;

#[get("/login")]
fn login(flash: Option<FlashMessage>) -> Template {
    let mut context: HashMap<&str, &str> = HashMap::new();

    if let Some(flash) = flash.as_ref() {
        context.insert("invalid_message", flash.msg());
    }

    Template::render("login", &context)
}

#[derive(FromForm)]
struct LoginPostModel {
    password: String
}

#[post("/login", data = "<model>")]
fn login_action(model: Form<LoginPostModel>, mut cookies: Cookies) -> Flash<Redirect> {
    let pass = model.password.eq("1234");

    if pass {
        cookies.add_private(Cookie::new("login", "true"));

        Flash::success(Redirect::to("/"), "Welcome!")
    } else {
        Flash::error(Redirect::to("/login"), "Invalid Password")
    }
}

#[get("/")]
fn index(flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Template, Redirect> {
    let has_auth = cookies.get_private("login").map(|cookie| cookie.value().eq("true")).unwrap_or(false);
    drop(cookies);

    if has_auth {
        let mut context: HashMap<&str, &str> = HashMap::new();

        if let Some(flash) = flash.as_ref() {
            context.insert("welcome_message", flash.msg());
        }

        Ok(Template::render("index", &context))
    } else {
        Err(Redirect::to("/login"))
    }
}

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

request::FlashMessage結構體可以當作請求守衛來使用,而request::FlashMessage結構實體的msg方法可以用來取得訊息內容並且順便將其從Cookie中刪除(方法內部會使用到http::Cookies來操作Cookie)。在先前的章節有提到,Rocket有限制http::Cookies在同一個時間只能夠有一個實體,所以這個例子中,我們在程式第50行手動使用drop來消滅用不到的http::Cookies實體,這樣才能確保request::FlashMessage結構實體的msg方法可以正常工作。

response::Flash結構體可以作為路由處理程序的函數的回傳值型別,用來將訊息存入客戶端的Cookie中。要建立出response::Flash結構實體,可以使用response::Flash結構體的new關聯函數,這個函數必須要手動替訊息設定一個「名稱」(name),這個名稱可以使用request::FlashMessage結構實體的name方法來取得。不管request::FlashMessage結構實體是只使用到name方法或是msg方法,還是兩個都有用,訊息都會從Cookie中被刪除。如果不想自己設定訊息的名稱,可以使用response::Flash結構體的successwarningerror關聯函數來產生已套用了預設的名稱的訊息,預設的名稱即分別為successwarningerror

而以下是indexlogin這兩個HTML模板的內容,可以參考看看。

<html>
    <head>
        <meta charset=UTF-8>
        <title>Admin</title>
    </head>
    <body>
        {{#if welcome_message}}
            <p>{{welcome_message}}</p>
        {{/if}}
        <p>Hello, old friend!</p>
    </body>
</html>
<html>
    <head>
        <meta charset=UTF-8>
        <title>Login</title>
    </head>
    <body>
        <form action="/login" method="POST">
            <input type="password" name="password" placeholder="Input your password">
            <button type="submit">Login</button>
        </form>
        {{#if invalid_message}}
            <p>{{invalid_message}}</p>
        {{/if}}
    </body>
</html>

總結

這個章節介紹了rocket_contrib這個Rocket官方提供的擴充套件中,有關處理HTTP回應的功能,以及Rocket框架提供的快閃訊息機制。下一個章節要繼續使用rocket_contrib,來讓我們的Rocket能夠搭配資料庫來使用!

下一章:資料庫的存取