模板引擎
模板引擎是 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)
模板方式的优势在于:
- 关注点分离:HTML 结构在模板文件中定义,业务逻辑在视图函数中处理
- 可维护性:修改页面结构只需改模板,不影响业务代码
- 安全性:Jinja2 自动转义特殊字符,防止 XSS 攻击
- 可复用:通过模板继承和宏实现代码复用
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>© 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:导入宏定义,适合共享可复用的逻辑单元
变量赋值
在模板中可以使用 set 和 with 创建变量:
<!-- 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 特殊字符会被转换为安全实体:
| 原字符 | 转义后 |
|---|---|
< | < |
> | > |
& | & |
" | " |
' | ' |
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><script>alert("xss")</script></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 的模板系统:
- Jinja2 语法:变量、过滤器、控制结构
- 模板继承:构建可维护的页面结构
- 宏系统:封装可复用的模板逻辑
- 安全机制:自动转义防止 XSS 攻击
- Flask 扩展:上下文处理器、全局函数
模板是 MVC 架构中 View 层的核心,掌握模板技术对于构建可维护的 Web 应用至关重要。
下一步
学习表单处理和验证,了解如何安全地处理用户输入。