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



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

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

Include Tera Templates for Rocket Framework

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

Crates.io

Cargo.toml

rocket-include-tera = "*"

使用方法

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

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

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

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

#[macro_use]
extern crate rocket;

#[macro_use]
extern crate rocket_include_tera;

use std::collections::HashMap;

use rocket::State;
use rocket_include_tera::{EtagIfNoneMatch, TeraContextManager, TeraResponse};
use serde_json::json;

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

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

    tera_response!(tera_cm, etag_if_none_match, "index", map)
}

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

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

    tera_response!(disable_minify tera_cm, etag_if_none_match, "index", map)
}

#[get("/2")]
fn index_2(cm: &State<TeraContextManager>, etag_if_none_match: EtagIfNoneMatch) -> TeraResponse {
    tera_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,
        });

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

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(TeraResponse::fairing(|tera| {
            tera_resources_initialize!(
                tera,
                "index" => "views/index.tera",
                "index2" => ("views", "index2.tera")
            );
        }))
        .mount("/", routes![index, index_disable_minify])
        .mount("/", routes![index_2])
}

以下是index.teraindex2.tera的原始碼,可以參考一下:

<!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-{{id + 1}}" type="text" placeholder="{{placeholder | escape}}">
</body>

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

例如:

rocket::build()
    .attach(TeraResponse::fairing_cache(|tera| {
        tera_resources_initialize!(
            tera,
            "index" => "views/index.tera",
            "index2" => ("views", "index2.tera")
        );

        512
    }))