跳到主要内容

模板引擎

模板引擎是 Web 框架的核心组件之一,负责将动态数据与静态模板结合,生成最终的 HTML 页面。Flask 使用 Jinja2 作为默认模板引擎,这是一个功能强大、安全且高效的模板系统。

理解模板引擎的作用

在 Web 开发中,我们经常需要根据不同的数据生成相似结构的页面。比如,一个博客文章列表页面,每篇文章的结构相同(标题、摘要、日期),但具体内容不同。如果每次都手动拼接 HTML 字符串,不仅繁琐而且容易出错。

模板引擎解决了这个问题:

# 不使用模板 - 手动拼接字符串(不推荐)
def render_user_list(users):
html = '<ul>'
for user in users:
html += f'<li>{user.name} - {user.email}</li>'
html += '</ul>'
return html

# 使用模板 - 清晰且安全(推荐)
@app.route('/users')
def user_list():
users = User.query.all()
return render_template('users.html', users=users)

模板方式的优势在于:

  1. 关注点分离:HTML 结构在模板文件中定义,业务逻辑在视图函数中处理
  2. 可维护性:修改页面结构只需改模板,不影响业务代码
  3. 安全性:Jinja2 自动转义特殊字符,防止 XSS 攻击
  4. 可复用:通过模板继承和宏实现代码复用

Jinja2 的核心概念

模板语法基础

Jinja2 模板是一个文本文件,可以包含以下元素:

语法用途示例
{{ ... }}输出表达式的值{{ user.name }}
{% ... %}控制语句(if、for 等){% if user %}...{% endif %}
{# ... #}注释(不会出现在输出中){# 这是注释 #}

理解这三种语法的区别很重要:双花括号用于输出,百分号用于逻辑控制,井号用于注释。

变量访问

在模板中访问变量有两种等价的方式:

<!-- 点号访问 -->
<p>{{ user.name }}</p>

<!-- 方括号访问 -->
<p>{{ user['name'] }}</p>

这两种方式在大多数情况下效果相同,但查找顺序略有不同:

  • user.name:先查找属性 getattr(user, 'name'),再查找键 user['name']
  • user['name']:先查找键 user['name'],再查找属性 getattr(user, 'name')

当对象的属性和字典键重名时,这种差异就会显现。通常建议使用点号语法,更符合 Python 的习惯。

访问嵌套数据

<!-- 对象属性 -->
<p>{{ user.profile.avatar }}</p>

<!-- 字典 -->
<p>{{ config['DATABASE_URL'] }}</p>

<!-- 列表 -->
<p>{{ items[0] }}</p>

<!-- 方法调用 -->
<p>{{ user.get_full_name() }}</p>

过滤器详解

过滤器是 Jinja2 最强大的功能之一,用于修改变量的输出格式。过滤器使用管道符 | 连接,可以链式调用。

内置过滤器分类

字符串处理

<!-- 大小写转换 -->
<p>{{ name|upper }}</p> <!-- HELLO -->
<p>{{ name|lower }}</p> <!-- hello -->
<p>{{ name|title }}</p> <!-- Hello World -->
<p>{{ name|capitalize }}</p> <!-- Hello world -->

<!-- 去除空白 -->
<p>{{ text|trim }}</p> <!-- 去除两端空白 -->
<p>{{ text|striptags }}</p> <!-- 去除 HTML 标签 -->

<!-- 截断 -->
<p>{{ long_text|truncate(50) }}</p> <!-- 截断到 50 字符 -->
<p>{{ long_text|truncate(50, leeway=5) }}</p> <!-- 允许超出 5 字符 -->
<p>{{ long_text|truncate(50, end='...') }}</p> <!-- 自定义结尾 -->

<!-- 其他 -->
<p>{{ text|replace('old', 'new') }}</p> <!-- 替换 -->
<p>{{ text|wordcount }}</p> <!-- 单词数 -->
<p>{{ text|escape }}</p> <!-- HTML 转义(等同于 |e) -->

数值处理

<!-- 四舍五入 -->
<p>{{ price|round }}</p> <!-- 整数 -->
<p>{{ price|round(2) }}</p> <!-- 保留两位小数 -->
<p>{{ price|round(2, 'floor') }}</p> <!-- 向下取整 -->

<!-- 绝对值 -->
<p>{{ number|abs }}</p>

<!-- 格式化 -->
<p>{{ value|int }}</p> <!-- 转整数 -->
<p>{{ value|float }}</p> <!-- 转浮点数 -->
<p>{{ value|string }}</p> <!-- 转字符串 -->

列表处理

<!-- 排序 -->
<p>{{ users|sort }}</p> <!-- 默认排序 -->
<p>{{ users|sort(attribute='name') }}</p> <!-- 按属性排序 -->
<p>{{ users|sort(reverse=true) }}</p> <!-- 降序 -->

<!-- 过滤 -->
<p>{{ users|selectattr('active', 'equalto', true) }}</p>

<!-- 映射 -->
<p>{{ users|map(attribute='name')|list }}</p> <!-- 提取所有名字 -->

<!-- 连接 -->
<p>{{ items|join(', ') }}</p> <!-- 用逗号连接 -->

<!-- 分组 -->
{% for group in users|groupby('department') %}
<h2>{{ group.grouper }}</h2>
{% for user in group.list %}
<p>{{ user.name }}</p>
{% endfor %}
{% endfor %}

默认值处理

<!-- 提供默认值 -->
<p>{{ user.name|default('Anonymous') }}</p>
<p>{{ user.name|default('Anonymous', true) }}</p> <!-- 仅对 undefined 生效 -->

<!-- 列表默认值 -->
<p>{{ items|default([]) }}</p>

<!-- 第一个/最后一个元素 -->
<p>{{ items|first }}</p>
<p>{{ items|last }}</p>

<!-- 长度 -->
<p>{{ items|length }}</p>
<p>{{ text|length }}</p>

安全相关

<!-- 标记为安全(不转义) -->
<p>{{ html_content|safe }}</p>

<!-- 强制转义 -->
<p>{{ user_input|e }}</p>

<!-- 转义后安全标记 -->
<p>{{ content|forceescape|safe }}</p>

自定义过滤器

当内置过滤器不能满足需求时,可以创建自定义过滤器:

from flask import Flask
from datetime import datetime

app = Flask(__name__)

# 方式一:使用装饰器
@app.template_filter('datetime_format')
def datetime_format(value, format='%Y-%m-%d %H:%M'):
"""格式化日期时间"""
if value is None:
return ""
return value.strftime(format)

# 方式二:直接注册
def reverse_filter(s):
"""反转字符串"""
return s[::-1]

app.jinja_env.filters['reverse'] = reverse_filter

# 使用装饰器但不指定名称(使用函数名)
@app.template_filter()
def bold(text):
"""包装为粗体"""
return f'<strong>{text}</strong>'

模板中使用:

<p>{{ post.created_at|datetime_format }}</p>
<p>{{ post.created_at|datetime_format('%Y年%m月%d日') }}</p>
<p>{{ message|reverse }}</p>
<p>{{ title|bold|safe }}</p>

控制结构

条件判断

Jinja2 的条件判断与 Python 类似:

<!-- 基本 if -->
{% if user %}
<p>欢迎,{{ user.name }}!</p>
{% endif %}

<!-- if-else -->
{% if user.is_admin %}
<a href="/admin">管理后台</a>
{% else %}
<a href="/login">请登录</a>
{% endif %}

<!-- if-elif-else -->
{% if score >= 90 %}
<span class="grade">优秀</span>
{% elif score >= 80 %}
<span class="grade">良好</span>
{% elif score >= 60 %}
<span class="grade">及格</span>
{% else %}
<span class="grade">不及格</span>
{% endif %}

<!-- 复杂条件 -->
{% if user and user.is_active and not user.is_banned %}
<p>用户状态正常</p>
{% endif %}

<!-- 使用测试器 -->
{% if user is defined %}
<p>用户:{{ user.name }}</p>
{% endif %}

{% if items is iterable %}
{% for item in items %}
<p>{{ item }}</p>
{% endfor %}
{% endif %}

循环

循环是模板中最常用的控制结构:

<!-- 基本循环 -->
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>

<!-- 循环带 else(列表为空时执行) -->
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% else %}
<li>暂无用户</li>
{% endfor %}
</ul>

<!-- 遍历字典 -->
<dl>
{% for key, value in config.items() %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>

<!-- 带条件过滤 -->
{% for user in users if user.is_active %}
<li>{{ user.name }}</li>
{% endfor %}

循环变量

在循环内部,Jinja2 提供了 loop 变量,包含循环的元信息:

<table>
{% for item in items %}
<tr class="{{ 'even' if loop.index is even else 'odd' }}">
<td>{{ loop.index }}</td> <!-- 索引,从 1 开始 -->
<td>{{ loop.index0 }}</td> <!-- 索引,从 0 开始 -->
<td>{{ item.name }}</td>
</tr>

<!-- 每 3 个元素后插入分隔线 -->
{% if loop.index % 3 == 0 and not loop.last %}
<tr><td colspan="3"><hr></td></tr>
{% endif %}
{% endfor %}
</table>

loop 变量的完整属性:

属性说明
loop.index当前迭代次数(从 1 开始)
loop.index0当前迭代次数(从 0 开始)
loop.revindex距离结束的迭代次数(从 1 开始)
loop.revindex0距离结束的迭代次数(从 0 开始)
loop.first是否是第一次迭代
loop.last是否是最后一次迭代
loop.length序列的总长度
loop.cycle在多个值之间循环
loop.changed(value)值是否与上次迭代不同
loop.previtem上一次迭代的项
loop.nextitem下一次迭代的项

cycle 辅助函数

<!-- 交替使用不同的 CSS 类 -->
{% for row in rows %}
<tr class="{{ loop.cycle('odd', 'even') }}">
<td>{{ row }}</td>
</tr>
{% endfor %}

递归循环

处理树形数据结构:

<ul class="menu">
{% for item in menu recursive %}
<li>
<a href="{{ item.url }}">{{ item.title }}</a>
{% if item.children %}
<ul class="submenu">{{ loop(item.children) }}</ul>
{% endif %}
</li>
{% endfor %}
</ul>

测试器

测试器用于检查变量的某种状态,语法为 变量 is 测试器

<!-- 定义检查 -->
{% if user is defined %}
<p>{{ user.name }}</p>
{% endif %}
{% if user is none %}
<p>用户不存在</p>
{% endif %}

<!-- 类型检查 -->
{% if items is iterable %}
{% for item in items %}...{% endfor %}
{% endif %}
{% if data is mapping %}
<p>这是一个字典</p>
{% endif %}
{% if value is string %}
<p>这是一个字符串</p>
{% endif %}
{% if value is number %}
<p>这是一个数字</p>
{% endif %}

<!-- 数值检查 -->
{% if value is even %}偶数{% endif %}
{% if value is odd %}奇数{% endif %}
{% if value is divisibleby 3 %}能被 3 整除{% endif %}
{% if value is greaterthan 10 %}大于 10{% endif %}

<!-- 字符串检查 -->
{% if email is string and email is matching('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') %}
<p>有效的邮箱格式</p>
{% endif %}

常用测试器列表:

测试器说明
defined变量是否已定义
undefined变量是否未定义
none变量是否为 None
string是否是字符串
number是否是数字
integer是否是整数
float是否是浮点数
iterable是否可迭代
mapping是否是字典
sequence是否是序列
callable是否可调用
even是否是偶数
odd是否是奇数
lower是否是小写
upper是否是大写

模板继承

模板继承是 Jinja2 最强大的功能,它允许定义一个基础模板,然后被子模板继承和覆盖。

基础模板(base.html)

基础模板定义页面的骨架结构,使用 {% block %} 标记可被覆盖的区域:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的网站{% endblock %}</title>

<!-- 全局样式 -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">

<!-- 页面特定样式 -->
{% block styles %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="container">
<a href="{{ url_for('main.index') }}" class="logo">Logo</a>
<ul class="nav-links">
<li><a href="{{ url_for('main.index') }}">首页</a></li>
<li><a href="{{ url_for('main.about') }}">关于</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">退出</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">登录</a></li>
{% endif %}
</ul>
</div>
</nav>

<!-- 主要内容区域 -->
<main class="container">
<!-- 消息闪现 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}

<!-- 页面内容 -->
{% block content %}{% endblock %}
</main>

<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>&copy; 2024 我的网站. All rights reserved.</p>
</div>
</footer>

<!-- 全局脚本 -->
<script src="{{ url_for('static', filename='js/base.js') }}"></script>

<!-- 页面特定脚本 -->
{% block scripts %}{% endblock %}
</body>
</html>

子模板

子模板使用 {% extends %} 继承基础模板,然后覆盖特定的块:

{% extends "base.html" %}

{% block title %}用户列表 - {{ super() }}{% endblock %}

{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}">
{% endblock %}

{% block content %}
<h1>用户列表</h1>

<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
<a href="{{ url_for('user.detail', id=user.id) }}">查看</a>
<a href="{{ url_for('user.edit', id=user.id) }}">编辑</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>

<!-- 分页 -->
{% if pagination.pages > 1 %}
<nav class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for('user.list', page=pagination.prev_num) }}">上一页</a>
{% endif %}

{% for p in pagination.iter_pages() %}
{% if p %}
{% if p == pagination.page %}
<span class="current">{{ p }}</span>
{% else %}
<a href="{{ url_for('user.list', page=p) }}">{{ p }}</a>
{% endif %}
{% else %}
<span class="ellipsis">...</span>
{% endif %}
{% endfor %}

{% if pagination.has_next %}
<a href="{{ url_for('user.list', page=pagination.next_num) }}">下一页</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}

{% block scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/users.js') }}"></script>
{% endblock %}

super() 函数

super() 用于在子模板中保留父模板的内容:

<!-- 父模板 -->
{% block head %}
<link rel="stylesheet" href="base.css">
{% endblock %}

<!-- 子模板 -->
{% block head %}
{{ super() }} <!-- 输出父模板的内容 -->
<link rel="stylesheet" href="page.css"> <!-- 追加内容 -->
{% endblock %}

<!-- 最终输出 -->
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="page.css">

多层继承

模板可以多层继承,super() 可以链式调用:

<!-- layout.html -->
{% block body %}
<p>Layout content</p>
{% endblock %}

<!-- page.html -->
{% extends "layout.html" %}
{% block body %}
<p>Page content</p>
{{ super() }}
{% endblock %}

<!-- article.html -->
{% extends "page.html" %}
{% block body %}
<p>Article content</p>
{{ super() }} <!-- 输出 page.html 的内容 -->
{{ super.super() }} <!-- 输出 layout.html 的内容(跳过 page.html)-->
{% endblock %}

必需块(Required Blocks)

可以定义必须在子模板中覆盖的块:

<!-- base.html -->
{% block content required %}{% endblock %}

<!-- 如果子模板没有覆盖 content 块,渲染时会报错 -->

宏(Macros)

宏类似于编程语言中的函数,用于封装可重用的模板片段。

定义和使用宏

<!-- 定义宏 -->
{% macro input(name, value='', type='text', placeholder='', required=false, class='') %}
<input
type="{{ type }}"
name="{{ name }}"
id="{{ name }}"
value="{{ value|e }}"
placeholder="{{ placeholder }}"
class="form-control {{ class }}"
{% if required %}required{% endif %}
>
{% endmacro %}

<!-- 使用宏 -->
<form>
{{ input('username', placeholder='请输入用户名', required=true) }}
{{ input('email', type='email', placeholder='请输入邮箱') }}
{{ input('password', type='password', placeholder='请输入密码', required=true) }}
</form>

表单字段宏

一个更实用的表单字段宏:

<!-- macros/forms.html -->
{% macro render_field(field, label=true, help_text='') %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
{% if label %}
<label for="{{ field.id }}" class="form-label">
{{ field.label.text }}
{% if field.flags.required %}
<span class="required">*</span>
{% endif %}
</label>
{% endif %}

{{ field(class_='form-control', **kwargs) }}

{% if help_text %}
<small class="form-text text-muted">{{ help_text }}</small>
{% endif %}

{% if field.errors %}
<ul class="error-list">
{% for error in field.errors %}
<li class="error text-danger">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}

{% macro render_form(form, action='', method='POST') %}
<form action="{{ action }}" method="{{ method }}" novalidate>
{{ form.hidden_tag() }}

{% for field in form %}
{% if field.type not in ['CSRFTokenField', 'SubmitField'] %}
{{ render_field(field) }}
{% endif %}
{% endfor %}

<button type="submit" class="btn btn-primary">
{{ form.submit.label.text if form.submit else '提交' }}
</button>
</form>
{% endmacro %}

使用宏:

{% from "macros/forms.html" import render_field, render_form %}

<!-- 使用 render_field -->
{{ render_field(form.username) }}
{{ render_field(form.email, help_text='我们不会泄露您的邮箱') }}

<!-- 使用 render_form -->
{{ render_form(form, action=url_for('auth.register')) }}

宏的内部变量

宏内部可以访问几个特殊变量:

{% macro example(arg1, arg2='default') %}
<p>宏名称: {{ name }}</p> <!-- example -->
<p>参数列表: {{ arguments }}</p> <!-- ('arg1', 'arg2') -->
<p>额外位置参数: {{ varargs }}</p> <!-- 未命名的位置参数 -->
<p>额外关键字参数: {{ kwargs }}</p> <!-- 未命名的关键字参数 -->
{% endmacro %}

{{ example('a', 'b', 'c', extra='value') }}

包含和导入

include 包含

include 用于在当前位置插入另一个模板的内容:

<!-- 包含整个模板 -->
{% include 'header.html' %}

<main>
{% block content %}{% endblock %}
</main>

{% include 'footer.html' %}

<!-- 忽略不存在的模板 -->
{% include 'optional.html' ignore missing %}

<!-- 包含时传递上下文 -->
{% include 'item.html' with context %}

<!-- 包含时不传递上下文 -->
{% include 'item.html' without context %}

import 导入

import 用于导入宏:

<!-- 导入整个模块 -->
{% import 'macros/forms.html' as forms %}
{{ forms.render_field(form.username) }}

<!-- 导入特定宏 -->
{% from 'macros/forms.html' import render_field, render_form %}
{{ render_field(form.username) }}

<!-- 导入并重命名 -->
{% from 'macros/forms.html' import render_field as field %}
{{ field(form.username) }}

<!-- 导入时包含上下文 -->
{% from 'macros/helpers.html' import render_user with context %}

import 与 include 的区别

  • include:直接插入模板内容,适合共享静态片段(页眉、页脚)
  • import:导入宏定义,适合共享可复用的逻辑单元

变量赋值

在模板中可以使用 setwith 创建变量:

<!-- set 赋值 -->
{% set title = "用户管理" %}
{% set users = get_users() %}

<!-- 多变量赋值 -->
{% set name, age = 'Alice', 25 %}

<!-- with 创建作用域 -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<!-- messages 在这里不可用 -->

注意作用域规则:在循环或块内赋值的变量不会影响外部作用域。

空白控制

Jinja2 提供了多种方式控制输出中的空白:

<!-- 默认行为:保留空白 -->
<div>
{% if True %}
<p>内容</p>
{% endif %}
</div>
<!-- 输出:
<div>

<p>内容</p>

</div>
-->

<!-- 使用减号去除空白 -->
<div>
{%- if True %}
<p>内容</p>
{%- endif %}
</div>
<!-- 输出:
<div><p>内容</p></div>
-->

<!-- 精确控制 -->
{%- for item in items -%}
{{ item }}
{%- endfor -%}
<!-- 输出紧凑的 item1item2item3 -->

在 Flask 中配置全局空白控制:

app.jinja_env.trim_blocks = True      # 删除块后的第一个换行符
app.jinja_env.lstrip_blocks = True # 删除块前的空白

自动转义与安全

理解自动转义

自动转义是防止 XSS(跨站脚本攻击)的关键机制。当启用自动转义时,HTML 特殊字符会被转换为安全实体:

原字符转义后
<&lt;
>&gt;
&&amp;
"&quot;
'&#39;

Flask 默认对 .html.htm.xml.xhtml 文件启用自动转义。

控制转义

<!-- 默认转义 -->
<p>{{ user_input }}</p>

<!-- 标记为安全(不转义) -->
<p>{{ trusted_html|safe }}</p>

<!-- 强制转义 -->
<p>{{ maybe_unsafe|e }}</p>

<!-- 临时禁用自动转义 -->
{% autoescape false %}
<p>{{ will_not_be_escaped }}</p>
{% endautoescape %}

在 Python 端标记安全字符串:

from markupsafe import Markup

@app.route('/page')
def page():
# 方式一:使用 Markup 包装
safe_html = Markup('<strong>安全的 HTML</strong>')

# 方式二:使用 Markup 组合
user_content = '<script>alert("xss")</script>'
safe_output = Markup('<div>{}</div>').format(user_content)
# 输出:<div>&lt;script&gt;alert("xss")&lt;/script&gt;</div>

return render_template('page.html', content=safe_html)

Flask 专属模板功能

Flask 在 Jinja2 基础上添加了一些特殊的全局变量和函数:

全局变量

<!-- config: 配置对象 -->
<p>调试模式: {{ config['DEBUG'] }}</p>

<!-- request: 当前请求对象 -->
<p>请求路径: {{ request.path }}</p>
<p>请求方法: {{ request.method }}</p>

<!-- session: 会话对象 -->
<p>用户 ID: {{ session.get('user_id') }}</p>

<!-- g: 全局对象 -->
<p>当前用户: {{ g.user.name }}</p>

<!-- current_user: Flask-Login 提供的用户对象 -->
<p>欢迎, {{ current_user.username }}</p>

全局函数

<!-- url_for: 生成 URL -->
<a href="{{ url_for('main.index') }}">首页</a>
<a href="{{ url_for('user.profile', id=123) }}">用户资料</a>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

<!-- get_flashed_messages: 获取闪现消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}

上下文处理器

上下文处理器用于在所有模板中自动注入变量或函数:

from flask import g, current_app

@app.context_processor
def inject_globals():
"""注入全局变量"""
return {
'site_name': '我的网站',
'current_year': datetime.now().year
}

@app.context_processor
def utility_processor():
"""注入工具函数"""
def format_currency(amount, currency='¥'):
return f'{currency}{amount:,.2f}'

def time_ago(dt):
"""计算时间差"""
delta = datetime.now() - dt
if delta.days > 365:
return f'{delta.days // 365} 年前'
elif delta.days > 30:
return f'{delta.days // 30} 个月前'
elif delta.days > 0:
return f'{delta.days} 天前'
elif delta.seconds > 3600:
return f'{delta.seconds // 3600} 小时前'
elif delta.seconds > 60:
return f'{delta.seconds // 60} 分钟前'
return '刚刚'

return dict(
format_currency=format_currency,
time_ago=time_ago
)

模板中使用:

<p>网站名称: {{ site_name }}</p>
<p>价格: {{ format_currency(99.99) }}</p>
<p>发布时间: {{ time_ago(post.created_at) }}</p>

流式渲染

对于大型模板或需要渐进式加载的场景,可以使用流式渲染:

from flask import stream_template

@app.route('/large-page')
def large_page():
"""流式渲染大型页面"""
return stream_template('large_page.html', items=generate_many_items())

流式渲染会逐步发送页面内容,而不是等待整个页面渲染完成,这在以下场景中特别有用:

  • 页面内容很多,需要加快首屏显示
  • 渲染耗时操作,需要显示进度
  • 大数据集渲染,减少内存占用

模板最佳实践

目录结构

templates/
├── base.html # 基础模板
├── index.html # 首页
├── auth/
│ ├── login.html
│ ├── register.html
│ └── reset_password.html
├── user/
│ ├── profile.html
│ └── settings.html
├── admin/
│ ├── dashboard.html
│ └── users.html
├── macros/
│ ├── forms.html # 表单宏
│ ├── pagination.html # 分页宏
│ └── utils.html # 工具宏
├── partials/
│ ├── header.html # 页眉
│ ├── footer.html # 页脚
│ └── sidebar.html # 侧边栏
└── errors/
├── 404.html
└── 500.html

性能优化

<!-- 避免重复查询 -->
{% set user_posts = user.posts %}
{% for post in user_posts %}
...
{% endfor %}

<!-- 使用 selectattr 过滤 -->
{% set active_users = users|selectattr('is_active', 'equalto', true)|list %}

<!-- 延迟加载宏(减少初始解析开销) -->
{% macro expensive_macro() %}
...
{% endmacro %}

安全建议

<!-- 始终转义用户输入 -->
<p>{{ user_input|e }}</p>

<!-- 明确标记可信内容 -->
<p>{{ markdown_content|safe }}</p>

<!-- 避免在模板中处理敏感逻辑 -->
<!-- 错误示例 -->
{% if user.password == input_password %} {# 不要这样做 #}
{% endif %}

<!-- 正确示例:在视图函数中验证 -->
{% if login_success %}
<p>登录成功</p>
{% endif %}

小结

本章深入讲解了 Flask 的模板系统:

  1. Jinja2 语法:变量、过滤器、控制结构
  2. 模板继承:构建可维护的页面结构
  3. 宏系统:封装可复用的模板逻辑
  4. 安全机制:自动转义防止 XSS 攻击
  5. Flask 扩展:上下文处理器、全局函数

模板是 MVC 架构中 View 层的核心,掌握模板技术对于构建可维护的 Web 应用至关重要。

下一步

学习表单处理和验证,了解如何安全地处理用户输入。