Rocket的rocket_contrib套件雖然有提供Template結構體,可以套用Handlebars模板來回應HTML資料,但是這些模板檔案必須要和程式執行檔分開來儲存才行,所以如果想要實現單檔執行的Web應用程式,就需要靠其它的方式來使用模板引擎。



先前雖然介紹過把外部檔案和Rust程式編譯在一起的方式,但如果要直接用這樣的方式搭配Rocket框架,讓它能夠套用存在於程式執行檔內部的Handlebars模板的話,會遇到一些問題。如果是使用include_bytes巨集的話,首當其衝會遇到編譯速度的問題,這個問題雖然也可以利用該篇文章介紹的「Lazy Static Include」套件獲得緩解,但還有個大問題是依然存在的。

Rocket應用程式所使用的Handlebars模板,在開發階段時也會跟著Rust程式不斷進行修改。如果將這些模板檔案編譯進程式執行檔中,那麼每當這些檔案被修改後,總是要重新編譯並重新執行Rocket應用程式才能看到修改後的結果,進而嚴重拖慢開發速度。

Include Handlebars Templates for Rocket Framework

「Include Handlebars Templates for Rocket Framework」是筆者開發的套件,是針對上述問題提出的解決方案。在非使用release模式來編譯程式專案時,這個套件不會將Handlebars的模板檔案與Rust程式編譯在一起,取而代之的是,將其讀取到記憶體中,並且在Rocket框架接收到HTTP請求時,依照所有模板檔案的修改日期來判斷模板引擎是否需要重新載入這些模板。這個套件也能夠將產生出來的HTML語法壓縮或是快取起來,除此之外還提供了HTTP的ETag機制。

Crates.io

https://crates.io/crates/rocket-include-handlebars

Cargo.toml

rocket-include-handlebars = "*"

使用方法

rocket_include_handlebars這個crate提供了HandlebarsResponse結構體,可以直接作為路由處理程序的函式回傳值的型別。在使用這個套件時,首先要用HandlebarsResponse結構體提供的fairing關聯函數來產生整流片,並註冊給Rocket來使用,這個整流片可以在Rocket應用程式狀態中建立一個大小為64(筆)的快取空間。fairing關聯函數必須傳入一個閉包,在這個閉包中,必須使用rocket_include_handlebars這個crate提供的handlebars_resources_initialize巨集,來設定要一同編譯進Rust程式的Handlebars模板檔案,而其設定的方式為:先指定模板檔案的鍵值,再指定模板檔案的路徑。在路由處理程序中,可以使用同樣由rocket_include_handlebars這個crate提供的handlebars_response巨集,傳入要回應的模板檔案的鍵值和上下文(context),就可以產生出對應的HandlebarsResponse結構實體。

預設的handlebars_response巨集所產生出來的HandlebarsResponse結構實體會主動壓縮HTML語法,如果不想要壓縮HTML語法的話,可以在第一個參數前加上disable_minify。如果只想要在release模式下才壓縮HTML語法的話,可以在第一個參數前加上auto_minify

如果要快取產生出來的HandlebarsResponse結構實體,可以再使用rocket_include_handlebars這個crate提供的handlebars_response_cache巨集。使用handlebars_response_cache巨集時,要去取得被註冊在Rocket應用程式狀態中的HandlebarsContextManager

舉例來說,在Cargo程式專案的根目錄底下的views目錄中,有index.hbsindex2.hbs這兩個Handlebars模板,若要使Rocket應用程式能夠套用這兩個模板檔案,並且使這兩個檔案能跟執行檔編譯在一起的話,程式可以寫成以下這樣:

#[macro_use]
extern crate rocket;

#[macro_use]
extern crate rocket_include_handlebars;

use std::collections::HashMap;

use rocket::State;

use rocket_include_handlebars::{EtagIfNoneMatch, HandlebarsContextManager, HandlebarsResponse};

use serde_json::json;

#[get("/")]
fn index(
    handlebars_cm: &State<HandlebarsContextManager>,
    etag_if_none_match: EtagIfNoneMatch,
) -> HandlebarsResponse {
    let mut map = HashMap::new();

    map.insert("title", "Title");
    map.insert("body", "Hello, world!");

    handlebars_response!(handlebars_cm, etag_if_none_match, "index", map)
}

#[get("/disable-minify")]
fn index_disable_minify(
    handlebars_cm: &State<HandlebarsContextManager>,
    etag_if_none_match: EtagIfNoneMatch,
) -> HandlebarsResponse {
    let mut map = HashMap::new();

    map.insert("title", "Title");
    map.insert("body", "Hello, world!");

    handlebars_response!(disable_minify handlebars_cm, etag_if_none_match, "index", map)
}

#[get("/2")]
fn index_2(
    cm: &State<HandlebarsContextManager>,
    etag_if_none_match: EtagIfNoneMatch,
) -> HandlebarsResponse {
    handlebars_response_cache!(cm, etag_if_none_match, "index-2", {
        println!("Generate index-2 and cache it...");

        let json = json! ({
            "title": "Title",
            "placeholder": "Hello, \"world!\"",
            "id": 0,
        });

        handlebars_response!(auto_minify cm, EtagIfNoneMatch::default(), "index2", json)
    })
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(HandlebarsResponse::fairing(|handlebars| {
            handlebars_resources_initialize!(
                handlebars,
                "index" => "examples/views/index.hbs",
                "index2" => ("examples", "views", "index2.hbs")
            );

            handlebars_helper!(inc: |x: i64| x + 1);

            handlebars.register_helper("inc", Box::new(inc));

            // NOTE: The above `inc` helper can be alternately added by enabling the `helper_inc` feature for the rocket_include_handlebars crate.
        }))
        .mount("/", routes![index, index_disable_minify])
        .mount("/", routes![index_2])
}

這邊要注意的是,在傳給HandlebarsResponse結構體的fairing關聯函數使用的閉包中,我們替handlebars變數註冊了我們自訂的helper函數。以下是index.hbsindex2.hbs的原始碼,可以參考一下:

<!DOCTYPE html>
<html>
<head>
    <meta charset=UTF-8>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{{title}}</title>
</head>
<body>
{{body}}
</body>
<!DOCTYPE html>
<html>
<head>
    <meta charset=UTF-8>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{{title}}</title>
</head>
<body>
<input id="input-{{inc id}}" type="text" placeholder="{{placeholder}}">
</body>

實際上,這個套件已經有替Handlebars模板引擎提供一些內建的額外helper函數,可以在Cargo.toml設定檔中啟用以下特色來直接引用有需要的helper函數,就不用自己再寫一次啦!

  • helper_inc:啟用inc函數,可以使數值加一。
  • helper_dec:啟用dec函數,可以使數值減一。
  • helper_eq_str:啟用eq_str函數,可以判斷兩個字串是否相同。
  • helper_ne_str:啟用ne_str函數,可以判斷兩個字串是否不相同。

如果覺得預設的快取空間太小的話,可以使用HandlebarsResponse結構體的fairing_cache關聯函數來產生整流片,這個fairing_cache關聯函數傳入的閉包可以直接回傳要在應用程式狀態中建立的快取空間太小。

例如:

rocket::build()
        .attach(HandlebarsResponse::fairing(|handlebars| {
            handlebars_resources_initialize!(
                handlebars,
                "index" => "examples/views/index.hbs",
                "index2" => ("examples", "views", "index2.hbs")
            );
 
            handlebars_helper!(inc: |x: i64| x + 1);
 
            handlebars.register_helper("inc", Box::new(inc));
 
            512
        }))