跳至内容

模板引擎

Tornado 内置了一套轻量模板引擎,语法与 Jinja2 接近但并不完全相同。模板在首次加载时编译为 Python 代码并缓存,性能较高。本文介绍模板的配置、变量渲染、控制语句、继承机制以及内置函数的使用。

基础配置

Applicationsettings 中配置模板目录:

settings = {
    "template_path": "templates",   # 模板根目录(相对于启动脚本)
    "autoescape": "xhtml_escape",   # 自动转义(默认开启,防 XSS)
}

目录结构示例:

project/
├── app.py
└── templates/
    ├── base.html
    ├── index.html
    └── user/
        └── profile.html

在 Handler 中渲染模板:

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render(
            "index.html",
            title="首页",
            user={"name": "Alice", "age": 28},
            items=["Python", "Go", "Rust"],
        )

模板语法

变量与表达式

使用双花括号 {{ }} 输出变量或表达式的值(输出内容会自动 HTML 转义):

<h1>{{ title }}</h1>
<p>用户:{{ user["name"] }},年龄:{{ user["age"] }}</p>
<p>标签数量:{{ len(items) }}</p>

输出不需要转义的原始 HTML 时,使用 {% raw %}

{% raw html_content %}

控制语句

控制语句使用 {% %} 包裹,所有块结构以 {% end %} 结束:

<!-- if / elif / else -->
{% if user["age"] >= 18 %}
  <p>成年用户</p>
{% elif user["age"] >= 12 %}
  <p>青少年用户</p>
{% else %}
  <p>儿童用户</p>
{% end %}

<!-- for 循环 -->
<ul>
{% for item in items %}
  <li>{{ item }}</li>
{% end %}
</ul>

<!-- while 循环(较少使用) -->
{% set i = 0 %}
{% while i < 3 %}
  <p>第 {{ i }} 行</p>
  {% set i = i + 1 %}
{% end %}

注释

{# 这是注释,不会出现在渲染结果中 #}

模板继承

Tornado 模板支持与 Jinja2 类似的继承机制,通过 {% extends %}{% block %} 实现布局复用。

基础模板 base.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>{% block title %}默认标题{% end %} - 我的站点</title>
  <link rel="stylesheet" href="{{ static_url('css/main.css') }}">
</head>
<body>
  <nav>{% block nav %}{% end %}</nav>
  <main>
    {% block content %}{% end %}
  </main>
  <footer>Copyright 2026</footer>
</body>
</html>

子模板 index.html

{% extends "base.html" %}

{% block title %}首页{% end %}

{% block content %}
  <h1>欢迎,{{ user_name }}!</h1>
  <ul>
    {% for item in items %}
      <li>{{ item }}</li>
    {% end %}
  </ul>
{% end %}

包含片段

{% include %} 将另一个模板文件嵌入当前位置,共享父模板的上下文变量:

{% include "components/header.html" %}
<main>{% block content %}{% end %}</main>
{% include "components/footer.html" %}

内置函数

模板中可直接调用以下内置函数:

函数说明
escape(s)HTML 转义字符串(将 <>& 等转义)
url_escape(s)URL 编码字符串
json_encode(obj)将对象序列化为 JSON 字符串
static_url(path)生成带版本哈希的静态文件 URL
reverse_url(name, *args)根据路由名称反解析 URL
xsrf_form_html()生成 XSRF 隐藏表单字段
<!-- 防 XSS:对不信任内容手动转义 -->
<p>{{ escape(user_input) }}</p>

<!-- 静态文件 URL(自动添加版本哈希,例如 /static/css/main.css?v=3a2b1c) -->
<link rel="stylesheet" href="{{ static_url('css/main.css') }}">

<!-- XSRF 防护(需在 settings 中开启 xsrf_cookies=True) -->
<form method="post">
  {% raw xsrf_form_html() %}
  <input type="text" name="username">
  <button type="submit">提交</button>
</form>

向模板传递自定义函数

render() 的关键字参数可以传递函数,在模板中直接调用:

import datetime

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        def format_date(ts):
            return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d")

        self.render(
            "index.html",
            articles=articles,
            format_date=format_date,
        )

模板中使用:

{% for article in articles %}
  <article>
    <h2>{{ article.title }}</h2>
    <time>{{ format_date(article.created_at) }}</time>
  </article>
{% end %}

XSRF 防护

Tornado 内置 XSRF(跨站请求伪造)防护,在 settings 中启用:

settings = {"xsrf_cookies": True}

启用后,所有 POSTPUTDELETE 请求都必须携带有效的 XSRF Token,否则返回 403。

  • HTML 表单:在表单中调用 {% raw xsrf_form_html() %} 自动插入隐藏字段。
  • AJAX 请求:从 Cookie _xsrf 读取 Token,并在请求头中以 X-XSRFToken 传递。
// jQuery 示例
function getCookie(name) {
  const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? match[2] : null;
}

$.ajax({
  url: "/api/submit",
  method: "POST",
  headers: {"X-XSRFToken": getCookie("_xsrf")},
  data: JSON.stringify(payload),
});
Tornado 的模板引擎功能完整但相对轻量。如果项目前端较复杂,推荐改用前后端分离架构(Tornado 仅提供 API),或引入 Jinja2(pip install jinja2 后可通过自定义 render() 集成)。
最后更新于