Rust模板引擎askama快速入门引擎
模板引擎很多时候还是很有用的,无论是后端渲染网页还是生成一些文本,其中以Jinja
比较出名,而本文的Rust
库askama
正是Jinja
的Rust
版实现,如果你对Jinja
的语法比较熟悉的话,使用askama
应该不会太难上手。
本文Cargo.toml
所有代码的依赖
[dependencies]
askama = "0.12.1"
axum = "0.7.4"
tokio = { version = "1.36.0", features = ["full"] }
快速入门
一般来说,模板都是在templates
目录下的一个文件,这里为了简单起见这里就不使用文件的形式了。
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
属性设置,另一种就是将模板单独放在templates
目录下的文件里面,比如下面这样的目录结构。
.
├── 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
的文件内容如下
hello {{name}}
渲染结果都是一样的: hello youerning.top
值得注意的是, 其实我们可以将name
字段设置成&'a str
的格式, 比如下面这样。
struct QuickStart<'a> {
name: &'a str,
}
这样可以避免没必要的内存拷贝以提升性能,但是吧,很多人可能看到生命周期就头疼,所以这里选择了效率不那么高的方式,也就是直接使用String
类型。
模板属性
askama
提供了一些可选的属性用于配置模板的一些行为,属性名如下
path
,比如template(path = "foo.html"))
, 通过该属性指定使用的模板文件source
, 比如template(source = "{{ foo }}")
, 直接使用字面值定义的模板内容,该属性需要配合ext
属性一起使用。ext
,比如template(source = "{{ foo }}", ext="txt")
, 指定模板的扩展名, 不同的扩展名代表不一样的内容,比如html
就需要额外的转义以避免XSS
攻击, 该属性不能和path
属性一起使用。print
,比如template(print = "code"))
,用于debug时使用,在运行的时候会打印宏生成的代码code
, 或者语法树ast
。escape
,比如template(escape = "none"))
, 可以通过配置none
来禁用转义。syntax
,比如template(syntax = "foo"))
, 指定要使用的语法配置, 如果你想自定义一种语法的话。
配置
在上一节有一个可能不太容易理解的配置,那就是syntax
,它的选项主要来源于askama
提供的语法配置选项,我们可以在根目录创建一个askama.toml
文件, askama
在编译的时候会读取这个文件,下面是一个官方示例:
[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=%{
, 而默认的是{{
, 这里的应用场景其实主要是用模板渲染模板的时候比较有用(嗯, 奇怪的需求),比如使用rust
渲染Python
或者golang
的模板, 后两者也是使用的一样的语法即{{
。
如果你使用过helm
可能会理解这个需求,假设需求是根据需求创建对应的helm
模板(chart
), helm
里面其实会用到Golang
的模板语法, 如果你想做一个渲染这种模板的需求,那么你可能就需要定义一套区别于Golang
渲染语法的askama
语法了。
除此之外我们还可以配置模板文件读取的位置,如果我们不喜欢templates
这个目录名的话。
[general]
# 默认情况是templates
dirs = ["tpls"]
语法
就像开头所说的, askama
算得上是Jinja
的Rust
版实现,所以语法几乎一致, 如果你会Jinja
的语法几乎就懂askama
的语法。
变量
这个比较简单,name
是模板结构体struct
中定义的字段,askama
对于要渲染的变量类型的要求是其实现了Display
这个trait
。
{{ name }}
赋值
在渲染的上下文中创建一个新的变量。
{% let len = name.len() %}
name length is {{ len }}
过滤器
对变量或者字面值做一个额外的处理,更多的内置的过滤器可以参考: https://djc.github.io/askama/filters.html
{{ "hello"|capitalize }} {{ name|capitalize }}
上面这部分的输出如下
Hello Youerning.top
要注意过滤|的两端不要有空格。
空格控制
渲染的时候可以选择是否将模板中的空格消除掉,比如
>> {{ name }} <<
的渲染结果是>> youerning.top <<
但是我们可以选择消除两边的空格,比如
>> {{- name -}} <<
的渲染结果是>>youerning.top<<
,可以看到, 模板语法把到非空字符之间的空格消除了,这种消除对于那些对空格敏感的格式很有用,比如yaml
,又或者渲染的结果需要美化之类的。
上面的例子是为了简单起见,一般来说消除的是控制结构比如if
和for
之类的语句的空格,比如下面这样。
{% if foo %}
{{- bar -}}
{% else if -%}
nothing
{%- endif %}
askama
一共支持三种空格控制的语法,第二个第三个没用过,这里直接复制的官方说明。
- Suppress (
-
) - Minimize (
~
) - Preserve (
+
)
函数
askama
支持三种形式的函数调用
- 模板的字段
- 静态函数
Struct/Trait
的函数实现, 也就是impl xxx
这样定义的函数
官方示例如下
#[derive(Template)]
#[template(source = "{{ foo(123) }}", ext = "txt")]
struct MyTemplate {
foo: fn(u32) -> String,
}
其他的函数定义形式这里就不赘述了。
模板继承
如果是做web开发,那么对继承应该不陌生,页面之间总是会存在可以复用的情况,所以我们可以将相同的部分抽离出来作为父模板
, 其他模板复用父模板
的相同部分,这和面向对象的继承差不多。
首先需要一个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
的内容如下:
* Item: {{ i }}
控制结构
之所以要使用模板引擎而不是format!
宏的一个很大的原因在于模板引擎支持控制结构,也就是循环和判断。
for循环
{% 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 %}
表达式
{{ 3 * 4 / 2 }}
{{ 26 / 2 % 7 }}
要注意表达式不要出现递归
注释
{# A Comment #}
结合web框架axum使用
最后来一个与axum
一起使用的例子作为结尾吧。
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!("服务监听在 127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
index.html
的内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ title }} - youerning.top</title>
</head>
<body>
<div id="content" style="text-align: center;">
{{content}}
</div>
</body>
</html>
总结
每门编程语言几乎都有模板引擎的,其实差别不大,但是根据语言特性的不同可能会稍稍改动,比如askama
还能在模板中使用match
语句, 以及变量赋值的时候需要在前面加个let
, 这些都是编程语言特性带来的。