Rust模板引擎askama快速入門
模板引擎很多時(shí)候還是很有用的,無論是后端渲染網(wǎng)頁還是生成一些文本,其中以Jinja比較出名,而本文的Rust庫askama正是Jinja的Rust版實(shí)現(xiàn),如果你對Jinja的語法比較熟悉的話,使用askama應(yīng)該不會太難上手。
本文Cargo.toml所有代碼的依賴
[dependencies]
askama = "0.12.1"
axum = "0.7.4"
tokio = { version = "1.36.0", features = ["full"] }
快速入門
一般來說,模板都是在templates目錄下的一個(gè)文件,這里為了簡單起見這里就不使用文件的形式了。
use askama::Template;
#[derive(Template)]
#[template(source = "Hello {{name}}", ext="txt")]
struct QuickStart {
name: String
}
fn main() {
let quickstart = QuickStart{name: String::from("youerning.top")};
println!("{}", quickstart.render().expect("渲染模板失敗:"));
}
askama支持兩種模板形式,一種就是上述這種直接通過souce屬性設(shè)置,另一種就是將模板單獨(dú)放在templates目錄下的文件里面,比如下面這樣的目錄結(jié)構(gòu)。
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── main.rs
└── templates
└── quickstart.txt
所以上面的例子還可以這樣寫。
use askama::Template;
#[derive(Template)]
#[template(path="quickstart.txt")]
struct QuickStart{
name: String,
}
fn main() {
let quick_start = QuickStart{name: String::from("youerning.top")};
println!("{}", quick_start.render().unwrap());
}
而quickstart.txt的文件內(nèi)容如下
hello {{name}}
渲染結(jié)果都是一樣的: hello youerning.top
值得注意的是, 其實(shí)我們可以將name字段設(shè)置成&'a str的格式, 比如下面這樣。
struct QuickStart<'a> {
name: &'a str,
}
這樣可以避免沒必要的內(nèi)存拷貝以提升性能,但是吧,很多人可能看到生命周期就頭疼,所以這里選擇了效率不那么高的方式,也就是直接使用String類型。
模板屬性
askama提供了一些可選的屬性用于配置模板的一些行為,屬性名如下
-
path,比如template(path = "foo.html")), 通過該屬性指定使用的模板文件 -
source, 比如template(source = "{{ foo }}"), 直接使用字面值定義的模板內(nèi)容,該屬性需要配合ext屬性一起使用。 -
ext,比如template(source = "{{ foo }}", ext="txt"), 指定模板的擴(kuò)展名, 不同的擴(kuò)展名代表不一樣的內(nèi)容,比如html就需要額外的轉(zhuǎn)義以避免XSS攻擊, 該屬性不能和path屬性一起使用。 -
print,比如template(print = "code")),用于debug時(shí)使用,在運(yùn)行的時(shí)候會打印宏生成的代碼code, 或者語法樹ast。 -
escape,比如template(escape = "none")), 可以通過配置none來禁用轉(zhuǎn)義。 -
syntax,比如template(syntax = "foo")), 指定要使用的語法配置, 如果你想自定義一種語法的話。
配置
在上一節(jié)有一個(gè)可能不太容易理解的配置,那就是syntax,它的選項(xiàng)主要來源于askama提供的語法配置選項(xiàng),我們可以在根目錄創(chuàng)建一個(gè)askama.toml文件, askama在編譯的時(shí)候會讀取這個(gè)文件,下面是一個(gè)官方示例:
[general]
default_syntax = "foo"
[[syntax]]
name = "foo"
block_start = "%{"
comment_start = "#{"
expr_end = "^^"
[[syntax]]
name = "bar"
block_start = "%%"
block_end = "%%"
comment_start = "%#"
expr_start = "%{"
為啥要配置一套額外的奇怪的語法?
比如上面的expr_start=%{, 而默認(rèn)的是{{, 這里的應(yīng)用場景其實(shí)主要是用模板渲染模板的時(shí)候比較有用(嗯, 奇怪的需求),比如使用rust渲染Python或者golang的模板, 后兩者也是使用的一樣的語法即{{。
如果你使用過helm可能會理解這個(gè)需求,假設(shè)需求是根據(jù)需求創(chuàng)建對應(yīng)的helm模板(chart), helm里面其實(shí)會用到Golang的模板語法, 如果你想做一個(gè)渲染這種模板的需求,那么你可能就需要定義一套區(qū)別于Golang渲染語法的askama語法了。
除此之外我們還可以配置模板文件讀取的位置,如果我們不喜歡templates這個(gè)目錄名的話。
[general]
# 默認(rèn)情況是templates
dirs = ["tpls"]
語法
就像開頭所說的, askama算得上是Jinja的Rust版實(shí)現(xiàn),所以語法幾乎一致, 如果你會Jinja的語法幾乎就懂askama的語法。
變量
這個(gè)比較簡單,name是模板結(jié)構(gòu)體struct中定義的字段,askama對于要渲染的變量類型的要求是其實(shí)現(xiàn)了Display這個(gè)trait。
{{ name }}
賦值
在渲染的上下文中創(chuàng)建一個(gè)新的變量。
{% let len = name.len() %}
name length is {{ len }}
過濾器
對變量或者字面值做一個(gè)額外的處理,更多的內(nèi)置的過濾器可以參考: https://djc.github.io/askama/filters.html
{{ "hello"|capitalize }} {{ name|capitalize }}
上面這部分的輸出如下
Hello Youerning.top
要注意過濾|的兩端不要有空格。
空格控制
渲染的時(shí)候可以選擇是否將模板中的空格消除掉,比如
>> {{ name }} <<
的渲染結(jié)果是>> youerning.top <<
但是我們可以選擇消除兩邊的空格,比如
>> {{- name -}} <<
的渲染結(jié)果是>>youerning.top<<,可以看到, 模板語法把到非空字符之間的空格消除了,這種消除對于那些對空格敏感的格式很有用,比如yaml,又或者渲染的結(jié)果需要美化之類的。
上面的例子是為了簡單起見,一般來說消除的是控制結(jié)構(gòu)比如if和for之類的語句的空格,比如下面這樣。
{% if foo %}
{{- bar -}}
{% else if -%}
nothing
{%- endif %}
askama一共支持三種空格控制的語法,第二個(gè)第三個(gè)沒用過,這里直接復(fù)制的官方說明。
- Suppress (
-) - Minimize (
~) - Preserve (
+)
函數(shù)
askama支持三種形式的函數(shù)調(diào)用
- 模板的字段
- 靜態(tài)函數(shù)
-
Struct/Trait的函數(shù)實(shí)現(xiàn), 也就是impl xxx這樣定義的函數(shù)
官方示例如下
#[derive(Template)]
#[template(source = "{{ foo(123) }}", ext = "txt")]
struct MyTemplate {
foo: fn(u32) -> String,
}
其他的函數(shù)定義形式這里就不贅述了。
模板繼承
如果是做web開發(fā),那么對繼承應(yīng)該不陌生,頁面之間總是會存在可以復(fù)用的情況,所以我們可以將相同的部分抽離出來作為父模板, 其他模板復(fù)用父模板的相同部分,這和面向?qū)ο蟮睦^承差不多。
首先需要一個(gè)base.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{{ title }} - My Site{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<div id="content">
{% block content %}<p>Placeholder content</p>{% endblock %}
</div>
</body>
</html>
其中{% block content %}{% endblock %}包裹的部分就是我們可以替換的部分。
子模板像下面這樣繼承即可。
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
<style>
</style>
{% endblock %}
{% block content %}
<h1>Index</h1>
<p>Hello, world!</p>
{% call super() %}
{% endblock %}
除了繼承,我們可以使用include指令來渲染一些通用的部分,比如:
{% for i in iter %}
{% include "item.html" %}
{% endfor %}
而item.html的內(nèi)容如下:
* Item: {{ i }}
控制結(jié)構(gòu)
之所以要使用模板引擎而不是format!宏的一個(gè)很大的原因在于模板引擎支持控制結(jié)構(gòu),也就是循環(huán)和判斷。
for循環(huán)
{% for char in name.chars() %}
-{{loop.index}}: {{ char|upper }}
{% endfor %}
if判斷
{% if name == 0 %}
youerning.top
{% else if name.len() == 1 %}
{{ name | upper }}
{% else %}
{{ name }}
{% endif %}
表達(dá)式
{{ 3 * 4 / 2 }}
{{ 26 / 2 % 7 }}
要注意表達(dá)式不要出現(xiàn)遞歸
注釋
{# A Comment #}
結(jié)合web框架axum使用
最后來一個(gè)與axum一起使用的例子作為結(jié)尾吧。
rust代碼如下:
use axum::{
http::StatusCode, response::{Html, IntoResponse, Response}, routing::get, Router
};
use askama::Template;
#[derive(Template, Default)]
#[template(path = "index.html")]
struct Index{
title: String,
content: String,
}
struct TemplateResponse<T>(T);
impl<T> IntoResponse for TemplateResponse<T>
where
T: Template,
{
fn into_response(self) -> Response {
match self.0.render() {
Ok(html) => Html(html).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to render template. Error: {err}"),
)
.into_response(),
}
}
}
pub async fn index() -> impl IntoResponse {
TemplateResponse(Index{
title: "askama快速入門".into(),
content: "做人最重要的就是開心啦.".into(),
})
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(index));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("服務(wù)監(jiān)聽在 127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
index.html的內(nèi)容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }} - youerning.top</title>
</head>
<body>
<div id="content" style="text-align: center;">
{{content}}
</div>
</body>
</html>
總結(jié)
每門編程語言幾乎都有模板引擎的,其實(shí)差別不大,但是根據(jù)語言特性的不同可能會稍稍改動,比如askama還能在模板中使用match語句, 以及變量賦值的時(shí)候需要在前面加個(gè)let, 這些都是編程語言特性帶來的。
參考鏈接
- https://docs.rs/askama/latest/askama/#the-template-attribute
- https://djc.github.io/askama/askama.html
