在這個章節中,會繼續介紹Rocket對不同資料格式提供的回應支援,包括JSON和MessagePack格式的回應,以及Handlebars和Tera這兩個HTML模板引擎。此外還會介紹Rocket框架本身提供的「快閃訊息」(Flash Message)機制。



JSON

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

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

例如:

#[macro_use]
extern crate serde;

#[macro_use]
extern crate rocket;

use rocket::serde::json::Json;

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

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

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![handler])
}

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

MessagePack

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

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

例如:

#[macro_use]
extern crate serde;

#[macro_use]
extern crate rocket;

use rocket::serde::msgpack::MsgPack;

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

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

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![handler])
}

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

模板引擎

模板引擎可以在原本文件的語法基礎上加上更強大的伺服器處理功能,來產生出最終的文件。在Rocket框架中若要使用模板引擎,可以使用官方另外提供的rocket_dyn_templates這個crate。rocket_dyn_templates支援Handlebars和Tera這兩種HTML模板引擎,前者被廣泛用於JavaScript生態圈;後者則是參考了在Python生態圈中被廣泛使用的Jinja2,再運用Rust程式語言所重新開發出來的HTML模板引擎,效能會更勝前者。

Handlebars的官方網站:

Tera的官方網站:

在Cargo程式專案的設定檔中引用rocket_dyn_templates,並依照自己要使用哪種模板引擎,來決定要啟用什麼特色,如下:

rocket_dyn_templates = { version = "0.1.0", features = ["handlebars", "tera"] }

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

模板引擎的整流片(Fairing)

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

程式如下:

use rocket_dyn_templates::Template;

...

rocket::build().attach(Template::fairing())

...

這個整流片會在Ignite事件發生時,去解析Rocket應用程式根目錄底下的templates目錄中的模板檔案。Handlebars的模板檔案使用的副檔名為.hbs,Tera的模板檔案使用的副檔名為.tera,如果同時使用相同檔案名稱(例如:index.html.hbsindex.html.tera)的Handlebars和Tera模板檔案,在這個解析的過程中就會被檢查出來,而使Rocket出現警告訊息。如果要改使用別的目錄存放HTML模板檔案,可以在Rocket框架的組態配置中添加template_dir項目來設定。

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

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

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

#[macro_use]
extern crate handlebars;

#[macro_use]
extern crate rocket;

use rocket_dyn_templates::Template;

...

rocket::build().attach(Template::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_dyn_templates::Template結構體作為路由處理程式函數的回傳值型別,程式寫法如下:

#[macro_use]
extern crate rocket;

use std::collections::HashMap;

use rocket_dyn_templates::Template;

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

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

#[launch]
fn rocket() -> _ {
    rocket::build().attach(Template::fairing()).mount("/", routes![index])
}

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

當路由處理程序的函數是回傳rocket_dyn_templates::Template時,HTTP回應中的標頭欄位除了之前提到的三個基本欄位外,還會多出Content-Type欄位,且該欄位的值會根據次副檔名來設定,例如.html.hbs就會被設定為text/html; charset=utf-8。若沒有次副檔名,則預設的Content-Type欄位為text/plain; charset=utf-8

傳給rocket_dyn_templates::Template結構體提供的render關聯函數所使用的「上下文」實體也可以用rocket_dyn_templatescontext!巨集來快速建立。如以下程式:

#[macro_use]
extern crate rocket;

use rocket_dyn_templates::{context, Template};

#[get("/")]
fn index() -> Template {
    Template::render("index", context! {
        foo: "bar"
    })
}

#[launch]
fn rocket() -> _ {
    rocket::build().attach(Template::fairing()).mount("/", routes![index])
}

快閃訊息(Flash Message)

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

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

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

#[macro_use]
extern crate rocket;

use std::collections::HashMap;

use rocket::{
    form::Form,
    http::{Cookie, CookieJar},
    request::FlashMessage,
    response::{Flash, Redirect},
};
use rocket_dyn_templates::Template;

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

#[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.message());
    }

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

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

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

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

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

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

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

        Ok(Template::render("index", &context))
    } else {
        Err(Box::new(Redirect::to(uri!(login()))))
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build().attach(Template::fairing()).mount("/", routes![login, login_action, index])
}

request::FlashMessage結構體可以當作請求守衛來使用,而request::FlashMessage結構實體的message方法可以用來取得訊息內容並且順便將其從Cookie中刪除。

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

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

<!DOCTYPE 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>
<!DOCTYPE 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對不同資料格式提供的回應支援,以及Rocket框架提供的快閃訊息機制。下一個章節要來讓我們的Rocket能夠搭配資料庫來使用!

下一章:資料庫的存取