Askama 模板秒编译:Jinja 语法,零成本渲染,Rust 类型安全兜底

Photos provided by Unsplash OR Pexels

Askama 介绍

Askama 是一个基于 Jinja 的模板渲染引擎,它在编译时从用户定义的结构体中生成类型安全的 Rust 代码,用于持有模板的上下文。它通过 Rust 的类型系统确保类型安全,模板被编译进 crate 中,以实现最佳性能。Askama 支持熟悉的 Jinja-like 语法,包括模板继承、循环、条件判断、宏、变量(不可变)、内置和自定义过滤器、白空间控制、可选的 HTML 转义,以及语法自定义。

主要特性包括:

  • 类型安全:通过 Rust 类型系统避免运行时错误。
  • 性能优化:模板在编译时转换为 Rust 代码。
  • UTF-8 验证:确保模板和输出是有效的 UTF-8。
  • 调试支持:便于模板开发。
  • 与 Rust 生态兼容:可集成多种 web 框架。

Askama 适用于生成 HTML、文本或其他基于文本的格式,尤其适合 web 应用中的服务器端渲染。

安装和配置

安装

在你的 Rust 项目中,将 Askama 添加到 Cargo.toml[dependencies] 部分:

[dependencies]
askama = "0.12"  # 或最新版本,例如 0.14

运行 cargo build 来安装依赖。

如果需要与 web 框架集成,使用 askama_web crate:

[dependencies]
askama_web = { version = "0.14", features = ["axum-0.8"] }  # 根据框架选择特征

配置

配置通过 crate 根目录下的 askama.toml 文件进行。该文件控制模板目录、白空间处理、自定义语法和转义器。

默认配置示例:

[general]
dirs = ["templates"]  # 模板目录,相对 crate 根目录
whitespace = "preserve"  # 默认保留白空间

白空间控制配置

  • preserve:默认,保留所有白空间,除非使用 - 标记抑制。
  • suppress:默认抑制白空间,使用 + 保留。
  • minimize:最小化白空间(保留一个字符,如换行),使用 ~ 控制。

可在模板结构体上覆盖:

#[derive(Template)]
#[template(whitespace = "suppress")]
struct MyTemplate;

自定义语法

[[syntax]] 部分定义:

[[syntax]]
name = "custom"
block_start = "%{"
block_end = "}%"
comment_start = "#{"
comment_end = "#}"
expr_start = "{{"
expr_end = "}}"

使用 default_syntax = "custom" 设置全局默认。

转义器

[[escaper]] 部分定义:

[[escaper]]
path = "askama::filters::Text"  # 无转义
extensions = ["txt", "md"]

默认转义基于文件扩展名(如 .html 使用 HTML 转义)。

最佳配置实践:使用 askama.toml 集中管理,避免每个模板单独设置。针对生产环境,选择 whitespace = "minimize" 以优化输出大小。

基本使用

Askama 的核心是定义一个持有上下文的结构体,派生 Template trait,并在编译时生成渲染代码。

简单示例

  1. 在 crate 根目录创建 templates 目录,并添加 hello.html
Hello, {{ name }}!
  1. 在 Rust 代码中定义上下文结构体:
use askama::Template;

#[derive(Template)]
#[template(path = "hello.html")]
struct HelloTemplate<'a> {
    name: &'a str,
}

fn main() {
    let hello = HelloTemplate { name: "world" };
    let output = hello.render().unwrap();
    println!("{}", output);  // 输出:Hello, world!
}

理论:Askama 在编译时解析模板,生成 render 方法的实现。该方法使用结构体的字段填充模板变量,确保类型匹配。如果类型不匹配,编译失败,实现类型安全。

渲染过程:调用 render() 返回 Result<String, Error>,处理任何运行时错误(如文件读取失败)。

模板语法

Askama 的语法类似于 Jinja,支持表达式、注释、白空间控制和块语句。

表达式

使用 {{ expr }} 输出值,支持 Rust 操作符(如 +, -, *)、方法调用和类型转换。

示例:

{{ 3 * 4 / 2 }}  <!-- 输出:6 -->
{{ (4 + 5) % 3 }}  <!-- 输出:0 -->
{{ name|upper }}  <!-- 如果 name = "askama",输出:ASKAMA -->

字符串连接:{{ a ~ " " ~ b }}

位运算符重命名:bitandbitorxor

HTML 特殊字符默认转义,除非使用 |safe

注释

使用 {# comment #},支持嵌套。

示例:

{# 这是一个注释 #}
{#
  多行注释
  {# 嵌套注释 #}
#}

白空间控制

默认保留白空间,使用 -+~ 控制:

  • -:抑制白空间。
  • +:保留白空间。
  • ~:最小化白空间。

示例:

{%- if foo -%}  <!-- 抑制周围白空间 -->
  {{ bar }}
{% endif %}

块语句

if / else

{% if users.len() == 0 %}
  No users
{% else if users.len() == 1 %}
  1 user
{% else %}
  {{ users.len() }} users
{% endif %}

支持 if let

{% if let Some(user) = user %}
  {{ user.name }}
{% else %}
  No user
{% endif %}

检查变量定义:{% if x is defined %}

match

用于枚举或 Option:

{% match item %}
  {% when Some with ("foo") %} Found literal foo
  {% when Some with (val) %} Found {{ val }}
  {% when None %} None
  {% else %} Other
{% endmatch %}

支持多模式:{% when 1 | 4 %}

for

{% for user in users %}
  <li>{{ user.name }}</li>
{% endfor %}

循环变量:loop.index (从 1 开始)、loop.index0 (从 0)、loop.firstloop.last

let 赋值

{% let name = user.name %}
{{ name.len() }}

支持阴影和复杂表达式。

上下文定义

上下文是持有模板变量的结构体或枚举。字段必须匹配模板变量,类型必须兼容(不可变)。

定义结构体

#[derive(Template)]
#[template(path = "footer.html", escape = "html")]
struct Footer {
    year: u32,
    title: String,
}

属性选项:

  • path:模板文件路径(相对 dirs)。
  • source:内联模板字符串。
  • ext:扩展名(影响转义)。
  • escape:转义模式 (“html”、“none”)。
  • print:打印模式 (“all”、“code”、“none”) 用于调试。
  • whitespace:白空间设置。

对于枚举:

#[derive(Template)]
#[template(path = "variant.html")]
enum Variant {
    A { field: String },
    B,
}

渲染:实例化结构体,调用 render()to_string()

高级:使用 render_with_values() 注入运行时值。

过滤器

过滤器使用 | 应用,支持链式。

内置过滤器

  • capitalize:首字母大写。
  • lower / upper:转小/大写。
  • escape / e:HTML 转义。
  • safe:标记安全(无转义)。
  • join:连接迭代器。
  • trim:去除白空间。
  • format:格式化字符串。
  • 更多见表格(例如 pluralizetruncateurlencode)。

示例:

{{ "hello" | upper | trim }}  <!-- HELLO -->
{{ array | join(", ") }}  <!-- foo, bar -->

自定义过滤器

filters 模块定义:

mod filters {
    use askama::Result;

    pub fn custom<T: std::fmt::Display>(value: &T) -> Result<String> {
        Ok(value.to_string().replace("a", "A"))
    }
}

使用:{{ text | custom }}

模板继承和宏

继承

基模板 (base.html):

<html>
  <title>{% block title %}Default{% endblock %}</title>
  <body>{% block content %}{% endblock %}</body>
</html>

子模板:

{% extends "base.html" %}
{% block title %}My Page{% endblock %}
{% block content %}
  <p>Hello</p>
  {% call super() %}  <!-- 调用父块内容 -->
{% endblock %}

include

{% include "header.html" %}

路径必须是字符串字面量。

定义:

{% macro heading(text) %}
  <h1>{{ text }}</h1>
{% endmacro %}

调用:

{% call heading("Title") %}

导入:

{% import "macros.html" as macros %}
{% call macros::heading("Title") %}

与 Web 框架集成

使用 askama_web 提供响应 trait 实现。

Axum 示例

依赖:

askama = "0.14"
askama_web = { version = "0.14", features = ["axum-0.8"] }
axum = "0.8"

代码:

use askama::Template;
use askama_web::WebTemplate;
use axum::{Router, routing::get};

#[derive(Template, WebTemplate)]
#[template(path = "hello.html")]
struct Hello { name: String }

async fn hello_handler() -> Hello {
    Hello { name: "world".to_string() }
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(hello_handler));
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

类似地,对于 Rocket、Actix 等,启用相应特征并派生 WebTemplate

理论:WebTemplate 实现框架的响应 trait,将模板渲染为 HTTP 响应,设置 Content-Type 为 text/html。

最佳实践

  • 类型安全优先:使用 match 和 if let 利用 Rust 类型检查。
  • 性能优化:避免运行时解析,使用编译时生成。
  • 安全:默认启用 HTML 转义,仅对可信内容使用 |safe。
  • 白空间管理:全局设置 minimize,使用 ~ 细粒度控制。
  • 模块化:使用继承、include 和宏重用代码。
  • 调试:设置 print = “all” 查看生成代码。
  • 集成:使用 askama_web 简化 web 响应。
  • 自定义:为特定格式定义转义器,避免手动 escape。
  • 测试:编写单元测试渲染输出。
  • 高效使用:保持上下文结构体简单,避免复杂逻辑移到 Rust 代码中。

实战示例

项目设置

创建一个简单 web 应用,使用 Axum 和 Askama 渲染用户列表。

  1. Cargo.toml
[package]
name = "askama-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
askama = "0.12"
askama_web = { version = "0.12", features = ["axum-0.6"] }  # 调整版本
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = "1"
  1. 创建 templates/base.html
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Askama Demo{% endblock %}</title>
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>
  1. 创建 templates/users.html
{% extends "base.html" %}
{% block title %}Users List{% endblock %}
{% block content %}
    <h1>Users</h1>
    <ul>
    {% for user in users %}
        <li>{{ user.name|capitalize }} (Age: {{ user.age }})</li>
    {% endfor %}
    </ul>
    {% if users.len() == 0 %}
        <p>No users found.</p>
    {% else %}
        <p>Total: {{ users.len() }} users.</p>
    {% endif %}
{% endblock %}
  1. src/main.rs
use askama::Template;
use askama_web::WebTemplate;
use axum::{Router, routing::get, Server};
use std::net::SocketAddr;

#[derive(Template, WebTemplate)]
#[template(path = "users.html")]
struct UsersTemplate {
    users: Vec<User>,
}

struct User {
    name: String,
    age: u32,
}

async fn users_handler() -> UsersTemplate {
    let users = vec![
        User { name: "alice".to_string(), age: 30 },
        User { name: "bob".to_string(), age: 25 },
    ];
    UsersTemplate { users }
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/users", get(users_handler));
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

运行 cargo run,访问 http://localhost:3000/users 查看渲染页面。

说明:这个示例展示了继承、循环、条件、过滤器。扩展时,可添加宏或自定义过滤器。

参考资料

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)