Rust模板引擎askama快速入门引擎

文章目录

模板引擎很多时候还是很有用的,无论是后端渲染网页还是生成一些文本,其中以Jinja比较出名,而本文的Rustaskama正是JinjaRust版实现,如果你对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算得上是JinjaRust版实现,所以语法几乎一致, 如果你会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,又或者渲染的结果需要美化之类的。

上面的例子是为了简单起见,一般来说消除的是控制结构比如iffor之类的语句的空格,比如下面这样。

{% if foo %}
  {{- bar -}}
{% else if -%}
  nothing
{%- endif %}

askama一共支持三种空格控制的语法,第二个第三个没用过,这里直接复制的官方说明。

  1. Suppress (-)
  2. Minimize (~)
  3. 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, 这些都是编程语言特性带来的。

参考链接