模板渲染 #

模板引擎概述 #

Actix Web 支持多种模板引擎:

模板引擎 特点 适用场景
Tera Jinja2 风格,功能丰富 动态网站
Askama 编译时模板,高性能 性能敏感场景
Handlebars JavaScript 风格 前端开发者友好
Askama Rust 模板,类型安全 类型安全要求高

Tera 模板引擎 #

添加依赖 #

toml
[dependencies]
actix-web = "4"
tera = "1"
serde = { version = "1", features = ["derive"] }

基本配置 #

rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use tera::{Context, Tera};

async fn index(tera: web::Data<Tera>) -> impl Responder {
    let mut context = Context::new();
    context.insert("title", "Welcome");
    context.insert("name", "World");
    
    let rendered = tera.render("index.html", &context).unwrap();
    
    HttpResponse::Ok()
        .content_type("text/html")
        .body(rendered)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let tera = Tera::new("templates/**/*.html").unwrap();
    
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(tera.clone()))
            .route("/", web::get().to(index))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

模板文件 #

templates/index.html:

html
<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

传递数据 #

rust
use serde::Serialize;

#[derive(Serialize)]
struct User {
    name: String,
    email: String,
}

async fn user_profile(tera: web::Data<Tera>) -> impl Responder {
    let user = User {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    let mut context = Context::new();
    context.insert("user", &user);
    
    let rendered = tera.render("profile.html", &context).unwrap();
    
    HttpResponse::Ok()
        .content_type("text/html")
        .body(rendered)
}

模板 templates/profile.html:

html
{% extends "base.html" %}

{% block content %}
<h1>User Profile</h1>
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
{% endblock %}

基础模板 #

templates/base.html:

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
    </nav>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        <p>&copy; 2024 My Website</p>
    </footer>
</body>
</html>

循环和条件 #

rust
async fn users(tera: web::Data<Tera>) -> impl Responder {
    let users = vec![
        User { name: "Alice".to_string(), email: "alice@example.com".to_string() },
        User { name: "Bob".to_string(), email: "bob@example.com".to_string() },
    ];
    
    let mut context = Context::new();
    context.insert("users", &users);
    context.insert("is_admin", &true);
    
    let rendered = tera.render("users.html", &context).unwrap();
    
    HttpResponse::Ok()
        .content_type("text/html")
        .body(rendered)
}

模板 templates/users.html:

html
{% extends "base.html" %}

{% block title %}Users{% endblock %}

{% block content %}
<h1>Users List</h1>

{% if is_admin %}
<a href="/users/new">Add New User</a>
{% endif %}

<ul>
{% for user in users %}
    <li>
        <strong>{{ user.name }}</strong>
        <span>{{ user.email }}</span>
    </li>
{% endfor %}
</ul>

{% if users | length == 0 %}
<p>No users found.</p>
{% endif %}
{% endblock %}

Askama 模板引擎 #

添加依赖 #

toml
[dependencies]
actix-web = "4"
askama = "0.12"

定义模板 #

rust
use askama::Template;

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

async fn hello_askama() -> impl Responder {
    let template = HelloTemplate { name: "World" };
    
    HttpResponse::Ok()
        .content_type("text/html")
        .body(template.render().unwrap())
}

模板 templates/hello.html:

html
<!DOCTYPE html>
<html>
<body>
    <h1>Hello, {{ name }}!</h1>
</body>
</html>

条件和循环 #

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

#[derive(Clone)]
struct User {
    name: String,
    email: String,
}

async fn users_askama() -> impl Responder {
    let template = UsersTemplate {
        users: vec![
            User { name: "Alice".to_string(), email: "alice@example.com".to_string() },
            User { name: "Bob".to_string(), email: "bob@example.com".to_string() },
        ],
        is_admin: true,
    };
    
    HttpResponse::Ok()
        .content_type("text/html")
        .body(template.render().unwrap())
}

模板 templates/users.html:

html
<!DOCTYPE html>
<html>
<body>
    <h1>Users</h1>
    
    {% if is_admin %}
    <a href="/users/new">Add User</a>
    {% endif %}
    
    <ul>
    {% for user in users %}
        <li>{{ user.name }} - {{ user.email }}</li>
    {% endfor %}
    </ul>
</body>
</html>

Handlebars 模板引擎 #

添加依赖 #

toml
[dependencies]
actix-web = "4"
handlebars = "5"
serde_json = "1"

基本配置 #

rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use handlebars::Handlebars;
use serde_json::json;

async fn index_hbs(hbs: web::Data<Handlebars<'_>>) -> impl Responder {
    let data = json!({
        "title": "Welcome",
        "name": "World"
    });
    
    let rendered = hbs.render("index", &data).unwrap();
    
    HttpResponse::Ok()
        .content_type("text/html")
        .body(rendered)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let mut hbs = Handlebars::new();
    hbs.register_template_file("index", "templates/index.hbs").unwrap();
    
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(hbs.clone()))
            .route("/", web::get().to(index_hbs))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

模板 templates/index.hbs:

html
<!DOCTYPE html>
<html>
<head>
    <title>{{title}}</title>
</head>
<body>
    <h1>Hello, {{name}}!</h1>
</body>
</html>

自定义模板响应器 #

rust
use actix_web::{HttpRequest, HttpResponse, Responder};
use tera::{Context, Tera};

struct HtmlTemplate {
    template: String,
    context: Context,
}

impl HtmlTemplate {
    fn new(template: &str, context: Context) -> Self {
        Self {
            template: template.to_string(),
            context,
        }
    }
}

impl Responder for HtmlTemplate {
    type Body = String;

    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
        let tera = req.app_data::<web::Data<Tera>>()
            .expect("Tera not configured");
        
        match tera.render(&self.template, &self.context) {
            Ok(html) => HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(html),
            Err(e) => HttpResponse::InternalServerError()
                .body(format!("Template error: {}", e)),
        }
    }
}

async fn index_auto() -> impl Responder {
    let mut context = Context::new();
    context.insert("title", "Welcome");
    
    HtmlTemplate::new("index.html", context)
}

错误处理 #

rust
use tera::Error as TeraError;

fn render_template(
    tera: &Tera,
    template: &str,
    context: &Context,
) -> Result<String, actix_web::Error> {
    tera.render(template, context)
        .map_err(|e| {
            match e {
                TeraError::TemplateNotFound(_) => {
                    actix_web::error::ErrorNotFound("Template not found")
                }
                _ => actix_web::error::ErrorInternalServerError(e),
            }
        })
}

async fn safe_render(tera: web::Data<Tera>) -> impl Responder {
    let mut context = Context::new();
    context.insert("title", "Welcome");
    
    match render_template(&tera, "index.html", &context) {
        Ok(html) => HttpResponse::Ok()
            .content_type("text/html")
            .body(html),
        Err(e) => e.into(),
    }
}

完整示例 #

rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::Serialize;
use tera::{Context, Tera};

#[derive(Serialize, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
}

async fn index(tera: web::Data<Tera>) -> impl Responder {
    let mut context = Context::new();
    context.insert("title", "Home");
    
    let html = tera.render("index.html", &context).unwrap();
    HttpResponse::Ok().content_type("text/html").body(html)
}

async fn users(tera: web::Data<Tera>) -> impl Responder {
    let users = vec![
        User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() },
        User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() },
    ];
    
    let mut context = Context::new();
    context.insert("title", "Users");
    context.insert("users", &users);
    
    let html = tera.render("users.html", &context).unwrap();
    HttpResponse::Ok().content_type("text/html").body(html)
}

async fn user_detail(
    path: web::Path<u32>,
    tera: web::Data<Tera>,
) -> impl Responder {
    let user = User {
        id: path.into_inner(),
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    let mut context = Context::new();
    context.insert("title", &format!("User - {}", user.name));
    context.insert("user", &user);
    
    let html = tera.render("user_detail.html", &context).unwrap();
    HttpResponse::Ok().content_type("text/html").body(html)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let tera = Tera::new("templates/**/*.html").unwrap();
    
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(tera.clone()))
            .route("/", web::get().to(index))
            .route("/users", web::get().to(users))
            .route("/users/{id}", web::get().to(user_detail))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

下一步 #

现在你已经掌握了模板渲染,继续学习 静态文件,深入了解静态资源服务!

最后更新:2026-03-29