乐于分享
好东西不私藏

Tera 模板引擎入门实战指南:从零开始构建Rust动态页面

Tera 模板引擎入门实战指南:从零开始构建Rust动态页面

Tera 是 Rust 生态中一款高性能模板引擎,灵感源自 Jinja2 与 Django 模板系统。本文从环境搭建、语法入门到实际项目应用,带你零基础掌握 Tera 的使用,涵盖变量插值、条件判断、循环迭代、模板继承、过滤器、自定义函数等核心功能,通过可运行的完整示例快速上手。

一、初识 Tera

Tera 是一个用 Rust 编写的现代模板引擎,它结合了 Jinja2 和 Django 模板的优点,提供了简洁而强大的模板语法。Tera 支持变量、条件语句、循环、宏、过滤器等功能,非常适合构建动态网页、邮件模板、配置文件等场景。

目前 Tera 已获得超过 4,200 个 GitHub Star,版本迭代活跃,是 Rust 生态中广受欢迎的模板引擎选择。

二、环境搭建与第一个模板

2.1 创建新项目

打开终端,执行以下命令创建一个新的 Rust 项目:

ounter(lineounter(linecargo new tera-democd tera-demo

2.2 添加依赖

编辑 Cargo.toml,添加 Tera 依赖:

ounter(lineounter(lineounter(line[dependencies]tera = "1.20"serde = { version = "1.0", features = ["derive"] }

serde 用于数据的序列化,是向模板传递数据的关键依赖。

如果你不需要默认功能(如自动转义、日期处理等),可以按如下方式禁用:

ounter(lineounter(line[dependencies]tera = { version = "1.20"default-features = false }

2.3 第一个 Hello World 模板

创建模板目录和文件:

ounter(linemkdir templates

在 templates 目录下创建 hello.html

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line<!DOCTYPE html><html><head>    <title>{{ title }}</title></head><body>    <h1>Hello, {{ name }}!</h1>    <p>当前时间:{{ now() | date(format="%Y-%m-%d %H:%M:%S") }}</p></body></html>

在 src/main.rs 中编写渲染代码:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineuse tera::{TeraContext};fn main() {    // 加载 templates 目录下所有 .html 模板文件    let tera = match Tera::new("templates/**/*.html") {        Ok(t) => t,        Err(e) => {            println!("模板解析错误: {}", e);            std::process::exit(1);        }    };    // 创建上下文并插入数据    let mut context = Context::new();    context.insert("title", &"Tera 入门教程");    context.insert("name", &"Rust 开发者");    // 渲染模板    let rendered = tera.render("hello.html", &context).unwrap();    println!("{}", rendered);}

运行项目:

ounter(linecargo run

你将在控制台看到生成的完整 HTML 内容。

三、模板语法详解

3.1 变量插值

Tera 使用 {{ variable }} 语法进行变量插值,支持嵌套属性访问:

ounter(lineounter(line<p>{{ user.name }}</p><p>{{ user.address.city }}</p>

3.2 条件判断

使用 {% if %} 语句进行条件控制:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{% if user.is_logged_in %}    <p>欢迎回来,{{ user.name }}!</p>{% elif user.is_guest %}    <p>欢迎访客!</p>{% else %}    <p>请先登录。</p>{% endif %}

Tera 还提供了丰富的内置测试器(tester),可以在条件中使用:

  • defined:检查变量是否已定义
  • odd / even:检查数字是否为奇数/偶数
  • string / number:检查值的类型
  • iterable:检查值是否可迭代
ounter(lineounter(lineounter(line{% if user.age is even %}    <p>{{ user.name }} 的年龄是偶数</p>{% endif %}

3.3 循环迭代

使用 {% for %} 语法遍历数组或可迭代对象:

ounter(lineounter(lineounter(lineounter(lineounter(line<ul>{% for product in products %}    <li>{{ product.name }} - ¥{{ product.price }}</li>{% endfor %}</ul>

遍历对象/Map 时可以使用 key-value 形式:

ounter(lineounter(lineounter(line{% for key, value in config %}    <p>{{ key }}: {{ value }}</p>{% endfor %}

循环中可用的特殊变量:

  • loop.index:当前迭代索引(从 1 开始)
  • loop.index0:当前迭代索引(从 0 开始)
  • loop.first:是否为第一次迭代
  • loop.last:是否为最后一次迭代
ounter(lineounter(lineounter(lineounter(lineounter(line{% for product in products %}    <div class="item{% if loop.first %}first{% endif %}">        第 {{ loop.index }} / {{ products | length }} 件商品:{{ product.name }}    </div>{% endfor %}

3.4 包含其他模板

使用 {% include %} 将一个模板的内容嵌入到当前位置:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line<header>    {% include "header.html" %}</header><main>    {{ content }}</main><footer>    {% include "footer.html" %}</footer>

注意:Tera 出于安全考虑,无法直接使用变量拼接路径(如 {% include "partials/" ~ name ~ ".html" %}),必须使用静态字符串。如果需要动态包含,可以使用自定义函数方案实现。

3.5 空白控制

使用 - 符号控制空白的去除:

ounter(lineounter(lineounter(line{%- if show_title -%}    <h1>{{ title }}</h1>{%- endif -%}
  • {%- 会去除标签前的空白
  • -%} 会去除标签后的空白

3.6 注释

ounter(line{# 这是一条注释,不会出现在生成的 HTML 中 #}

四、模板继承

模板继承是 Tera 的核心特性之一,允许你定义基础布局,并在子模板中填充具体内容。

4.1 创建基础模板

创建 templates/base.html

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line<!DOCTYPE html><html><head>    <metacharset="UTF-8">    <title>{% block title %}我的网站{% endblock title %}</title>    <linkrel="stylesheet"href="/css/style.css"></head><body>    <header>        <h1>{% block header %}网站标题{% endblock header %}</h1>    </header>    <main>        {% block content %}        <p>默认内容</p>        {% endblock content %}    </main>    <footer>        <p>© 2024 我的网站</p>    </footer></body></html>

{% block %} 标签定义了可被覆盖的占位区域。

4.2 创建继承的子模板

创建 templates/page.html

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{% extends "base.html" %}{% block title %}关于我们 - {{ super() }}{% endblock title %}{% block header %}关于我们{% endblock header %}{% block content %}    <article>        <h2>关于这个项目</h2>        <p>这是一个使用 Tera 模板引擎构建的 Rust 网站。</p>    </article>{% endblock content %}

{{ super() }} 用于继承父块的内容,将其追加到当前输出中。

4.3 多层继承

Tera 支持多层模板继承,即 A extends B,B extends C。

ounter(lineounter(lineounter(linebase.html  └── layout.html        └── page.html

每一层都可以覆盖特定的 block,形成灵活的模板体系。

五、完整实战案例

以下我们将创建一个完整的静态网站生成器示例,展示 Tera 在真实项目中的应用。

5.1 项目结构

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linetera-demo/├── Cargo.toml├── src/│   └── main.rs├── templates/│   ├── base.html│   ├── index.html│   ├── post.html│   └── macros.html└── output/

5.2 基础模板

templates/base.html

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line<!DOCTYPE html><html><head>    <metacharset="UTF-8">    <metaname="viewport"content="width=device-width, initial-scale=1.0">    <title>{% block title %}我的博客{% endblock title %} - Rust 学习笔记</title>    <style>        body { font-family: system-ui, sans-serif; max-width800pxmargin0 auto; padding1rem; }        nav { background#f0f0f0padding1remmargin-bottom2remborder-radius8px; }        nav a { margin-right1remtext-decoration: none; color#0066cc; }        footer { margin-top2remtext-align: center; color#666border-top1px solid #eeepadding-top1rem; }        article { margin-bottom2remborder-bottom1px solid #eee; }        h2 { margin-bottom0.25rem; }        .meta { color#666font-size0.875remmargin-bottom1rem; }    </style></head><body>    <nav>        <ahref="/">首页</a>        <ahref="/posts">文章列表</a>        <ahref="/about">关于</a>    </nav>    {% block content %}{% endblock content %}    <footer>        <p>© 2024 Rust 学习笔记 | 由 Tera 生成</p>    </footer></body></html>

5.3 首页模板

templates/index.html

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{% extends "base.html" %}{% block title %}首页{% endblock title %}{% block content %}    <h1>欢迎来到我的博客</h1>    <p>这里记录了我的 Rust 学习历程和技术笔记。</p>    <h2>最新文章</h2>    {% for post in posts %}        {% if loop.index <= 5 %}        <article>            <h2><ahref="/posts/{{ post.slug }}">{{ post.title }}</a></h2>            <divclass="meta">                发布于 {{ post.date | date(format="%Y年%m月%d日") }} | 阅读 {{ post.read_time }} 分钟            </div>            <p>{{ post.summary | truncate(length=150) }}</p>        </article>        {% endif %}    {% endfor %}    <p><ahref="/posts">查看全部文章 →</a></p>{% endblock content %}

5.4 文章详情模板

templates/post.html

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line{% extends "base.html" %}{% block title %}{{ post.title }}{% endblock title %}{% block content %}    <article>        <h1>{{ post.title }}</h1>        <divclass="meta">            作者:{{ post.author }} | 发布于 {{ post.date | date(format="%Y年%m月%d日") }}        </div>        {% if post.tags %}        <divclass="tags">            标签:            {% for tag in post.tags %}                <ahref="/tags/{{ tag | slugify }}">{{ tag }}</a>{% if not loop.last %}, {% endif %}            {% endfor %}        </div>        {% endif %}        <divclass="content">            {{ post.content | safe }}        </div>        <hr>        <p><ahref="/">← 返回首页</a></p>    </article>{% endblock content %}

5.5 Rust 后端代码

src/main.rs

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineuse tera::{Tera, Context};use serde::Serialize;use std::fs;use std::path::Path;#[derive(Serialize)]struct Post {    title: String,    slug: String,    author: String,    date: String,    read_time: u32,    summary: String,    tags: Vec<String>,    content: String,}fn main() -> Result<(), Box<dyn std::error::Error>> {    // 加载并编译模板    let tera = Tera::new("templates/**/*.html")?;    // 创建输出目录    let output_dir = Path::new("output");    if !output_dir.exists() {        fs::create_dir_all(output_dir)?;    }    // 准备示例数据(真实项目中可从 Markdown 文件读取)    let posts = vec![        Post {            title"Rust 入门指南".to_string(),            slug"rust-beginner-guide".to_string(),            author"张三".to_string(),            date"2024-01-15".to_string(),            read_time8,            summary"本文介绍 Rust 语言的基本概念、安装方法和第一个程序...".to_string(),            tags: vec!["Rust".to_string(), "入门".to_string()],            content"<p>Rust 是一门系统级编程语言...</p>".to_string(),        },        Post {            title"Tera 模板引擎完全指南".to_string(),            slug"tera-template-guide".to_string(),            author"张三".to_string(),            date"2024-01-20".to_string(),            read_time12,            summary"详细讲解 Tera 模板引擎的使用方法和最佳实践...".to_string(),            tags: vec!["Rust".to_string(), "Tera".to_string(), "模板".to_string()],            content"<p>Tera 是一款优秀的 Rust 模板引擎...</p>".to_string(),        },    ];    // 渲染首页    let mut ctx = Context::new();    ctx.insert("posts", &posts);    let index_html = tera.render("index.html", &ctx)?;    fs::write(output_dir.join("index.html"), index_html)?;    println!("✅ 已生成: output/index.html");    // 渲染每篇文章的详情页    for post in &posts {        let mut post_ctx = Context::new();        post_ctx.insert("post", &post);        let post_html = tera.render("post.html", &post_ctx)?;        fs::write(output_dir.join(format!("{}.html", post.slug)), post_html)?;        println!("✅ 已生成: output/{}.html", post.slug);    }    println!("✨ 所有模板已渲染完成!");    Ok(())}

5.6 运行结果

执行 cargo run 后,output/ 目录下将生成静态 HTML 文件,可以直接部署到任何 Web 服务器上查看。

六、内置过滤器

Tera 提供了丰富的内置过滤器,用于数据的格式化与转换。以下是最常用的过滤器:

过滤器
说明
示例
safe
标记内容为安全的 HTML,不对其进行转义
{{ html_content \| safe }}
escape
HTML 转义
{{ user_input \| escape }}
lower
 / upper
转换为小写/大写
{{ name \| lower }}
title
将单词首字母大写
{{ "hello world" \| title }}
trim
去除首尾空白
{{ text \| trim }}
truncate
截断字符串
{{ long_text \| truncate(length=100) }}
date
格式化日期
{{ timestamp \| date(format="%Y-%m-%d") }}
slugify
转换为 URL 友好的 slug 格式
{{ title \| slugify }}
urlencode
URL 编码
{{ query \| urlencode }}
json_encode
转换为 JSON 字符串
{{ data \| json_encode }}
nth
获取数组第 n 个元素
{{ arr \| nth(n=0) }}

过滤器链式调用

多个过滤器可以串联使用:

ounter(lineounter(line<p>{{ user_comment | escape | truncate(length=200) }}</p><p>{{ post_title | lower | slugify }}</p>

七、自定义过滤器/函数

7.1 注册自定义过滤器

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineuse tera::{TeraResultValue, from_value, to_value};fn uppercase_filter(value: &Value_args: &HashMap<StringValue>) -> Result<Value> {    let s = from_value::<String>(value.clone())?;    Ok(to_value(s.to_uppercase()).unwrap())}fn main() {    let mut tera = Tera::new("templates/**/*.html").unwrap();    tera.register_filter("uppercase", uppercase_filter);    // ...}

在模板中使用:

ounter(line<p>{{ "hello world" | uppercase }}</p>

7.2 注册自定义全局函数

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineuse tera::{TeraResultValue, from_value, to_value};use std::collections::HashMap;fn greet(args: &HashMap<StringValue>) -> Result<Value> {    let name = match args.get("name") {        Some(val) => from_value::<String>(val.clone())?,        None => "Guest".to_string(),    };    Ok(to_value(format!("Hello, {}!", name)).unwrap())}fn main() {    let mut tera = Tera::new("templates/**/*.html").unwrap();    tera.register_function("greet", greet);    // ...}

在模板中使用:

ounter(line<p>{{ greet(name="Alice") }}</p>

八、模板内嵌到二进制文件

在实际生产环境中,将模板文件嵌入到 Rust 二进制文件中可以实现单文件部署,避免运行时文件路径问题。

8.1 添加依赖

ounter(lineounter(lineounter(line[dependencies]tera = "1.20"include_dir = "0.7"

8.2 实现模板内嵌

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineuse include_dir::{include_dir, Dir};use tera::Tera;static TEMPLATE_DIR: Dir = include_dir!("templates");pub fn load_embedded_templates() -> Tera {    let mut tera = Tera::default();    // 收集所有模板文件    let templates: Vec<(&str, &str)> = TEMPLATE_DIR        .find("**/*.html")        .unwrap()        .filter_map(|entry| {            let file = entry.as_file()?;            let name = file.path().to_str()?;            let content = file.contents_utf8()?;            Some((name, content))        })        .collect();    // 一次性添加所有模板(自动处理继承依赖顺序)    tera.add_raw_templates(templates).unwrap();    tera}

8.3 开发与生产环境切换

通过条件编译,在开发时使用外部文件(便于调试),在生产时使用内嵌模板:

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line#[cfg(debug_assertions)]pub fn create_tera() -> Tera {    Tera::new("templates/**/*").expect("Failed to load templates")}#[cfg(not(debug_assertions))]pub fn create_tera() -> Tera {    use include_dir::{include_dir, Dir};    static TEMPLATE_DIRDir = include_dir!("templates");    let mut tera = Tera::default();    let templates = TEMPLATE_DIR        .find("**/*.html")        .unwrap()        .filter_map(|entry| {            let file = entry.as_file()?;            Some((file.path().to_str()?, file.contents_utf8()?))        });    tera.add_raw_templates(templates).unwrap();    tera}

九、安全最佳实践

  1. 自动转义:Tera 默认会对 .html.htm 和 .xml 文件的内容进行自动转义,防止 XSS 攻击。这遵循 OWASP 推荐的安全标准。
ounter(lineounter(lineounter(lineounter(lineounter(line// 自定义需要转义的文件扩展名tera.autoescape_on(vec![".html"".html.php"]);// 完全禁用自动转义(仅当确定内容安全时使用)tera.autoescape_on(vec![]);
  1. 严格模式:Tera 的严格模式会在变量未定义时报错,有助于捕获模板中的拼写错误。

  2. 路径安全:在使用动态模板包含时,永远不要允许用户直接控制模板路径,防止目录遍历攻击。

  3. 内容过滤:对用户输入的内容应用适当的过滤器进行转义或清理。

十、常见问题与解决方案

10.1 模板继承顺序问题

当模板之间存在继承关系时,如果子模板先于父模板被加载,会导致解析错误。解决方案是使用 add_raw_templates 一次性添加所有模板,让 Tera 自动处理依赖关系。

10.2 动态模板包含

Tera 不允许使用变量拼接路径进行模板包含。解决方案是通过 Rust 端实现自定义函数来动态获取模板:

ounter(lineounter(lineounter(lineounter(lineounter(linefn get_dynamic_template(args: &HashMap<StringValue>) -> Result<Value> {    let template_name = from_value::<String>(args.get("name").unwrap().clone())?;    let rendered = TEMPLATES.render(&template_name, &Context::new())?;    Ok(to_value(rendered)?)}

在模板中使用 {{ get_dynamic_template(name=var) | safe }}

10.3 性能优化

使用 lazy_static 或 once_cell 将 Tera 实例声明为全局静态变量,确保模板只编译一次:

ounter(lineounter(lineounter(lineounter(lineounter(lineuse once_cell::sync::Lazy;static TEMPLATES: Lazy<Tera> = Lazy::new(|| {    Tera::new("templates/**/*.html").expect("Failed to load templates")});

十一、总结

本文从零开始介绍了 Tera 模板引擎的完整使用路径,涵盖了项目配置、基础语法、模板继承、实战案例以及高级特性。核心要点包括:

  • 使用 Tera::new() 加载模板目录,通过 Context 传递数据
  • {% block %}{% extends %}{% include %} 实现模块化模板设计
  • 丰富的内置过滤器和测试器满足常见格式化需求
  • 支持自定义过滤器和函数扩展模板功能
  • 可将模板嵌入二进制文件实现单文件部署

Tera 的设计范式偏向传统服务端渲染(如 Jinja2),与 React/Vue 等现代前端框架的组件化思路有本质区别。基于这些基础,你可以进一步探索宏系统、自定义测试器等高级特性,构建更灵活的 Web 应用。

无论身在何处

有我不再孤单孤单

长按识别二维码关注我们