模板引擎 Jinja2
2026/3/20大约 15 分钟
模板引擎 Jinja2
第一章:Jinja2 概述
什么是 Jinja2?
Jinja2 是一个现代的、设计优雅的 Python 模板引擎。它由 Armin Ronacher(Flask 作者)开发,是 Flask 的默认模板引擎。Jinja2 的语法灵感来自 Django 模板,但功能更加强大。
核心特性
- 沙箱执行:模板代码在受限环境中执行,防止不安全操作
- 自动 HTML 转义:防止 XSS 攻击
- 模板继承:强大的布局复用机制
- 即时编译:模板编译为 Python 代码,执行效率高
- 可选的预编译:支持将模板预编译为 Python 字节码
- 异步支持:支持异步迭代器和协程
基本语法
Jinja2 使用特殊的分隔符来区分模板代码和普通文本:
| 语法 | 用途 | 示例 |
|---|---|---|
{{ }} | 输出表达式 | {{ user.name }} |
{% %} | 控制语句 | {% if user %}...{% endif %} |
{# #} | 注释 | {# 这是注释 #} |
第二章:变量与表达式
变量输出
<!-- 基本变量 -->
<h1>{{ title }}</h1>
<p>Welcome, {{ user.name }}!</p>
<!-- 字典访问 -->
<p>Email: {{ user['email'] }}</p>
<p>Email: {{ user.email }}</p>
<!-- 列表访问 -->
<p>First item: {{ items[0] }}</p>
<p>Last item: {{ items[-1] }}</p>
<!-- 默认值 -->
<p>{{ username | default('Guest') }}</p>
<!-- 三元表达式 -->
<p>{{ 'Active' if user.is_active else 'Inactive' }}</p>
表达式类型
<!-- 字面量 -->
{{ "Hello" }}
<!-- 字符串 -->
{{ 42 }}
<!-- 整数 -->
{{ 3.14 }}
<!-- 浮点数 -->
{{ true }}
<!-- 布尔值 -->
{{ ['a', 'b', 'c'] }}
<!-- 列表 -->
{{ {'key': 'value'} }}
<!-- 字典 -->
{{ (1, 2, 3) }}
<!-- 元组 -->
<!-- 数学运算 -->
{{ 1 + 2 }}
<!-- 加法: 3 -->
{{ 5 - 3 }}
<!-- 减法: 2 -->
{{ 2 * 3 }}
<!-- 乘法: 6 -->
{{ 10 / 3 }}
<!-- 除法: 3.333... -->
{{ 10 // 3 }}
<!-- 整除: 3 -->
{{ 10 % 3 }}
<!-- 取模: 1 -->
{{ 2 ** 8 }}
<!-- 幂运算: 256 -->
<!-- 比较运算 -->
{{ 1 == 1 }}
<!-- 等于 -->
{{ 1 != 2 }}
<!-- 不等于 -->
{{ 1 < 2 }}
<!-- 小于 -->
{{ 2 > 1 }}
<!-- 大于 -->
{{ 1 <= 1 }}
<!-- 小于等于 -->
{{ 2 >= 2 }}
<!-- 大于等于 -->
<!-- 逻辑运算 -->
{{ true and false }}
<!-- 逻辑与 -->
{{ true or false }}
<!-- 逻辑或 -->
{{ not false }}
<!-- 逻辑非 -->
<!-- 字符串拼接 -->
{{ "Hello, " ~ user.name ~ "!" }}
<!-- in 运算符 -->
{{ 'admin' in user.roles }}
<!-- is 测试 -->
{{ value is defined }} {{ value is none }}
访问嵌套对象
# 视图函数传递复杂数据
@app.route('/')
def index():
data = {
'user': {
'name': 'Alice',
'profile': {
'avatar': '/images/alice.jpg',
'bio': 'Python Developer'
},
'posts': [
{'title': 'First Post', 'views': 100},
{'title': 'Second Post', 'views': 200}
]
}
}
return render_template('index.html', **data)
<!-- 模板中访问 -->
<h1>{{ user.name }}</h1>
<img src="{{ user.profile.avatar }}" />
<p>{{ user.profile.bio }}</p>
<!-- 访问列表中的对象 -->
<h2>{{ user.posts[0].title }}</h2>
<p>Views: {{ user.posts[0].views }}</p>
<!-- 遍历嵌套数据 -->
{% for post in user.posts %}
<article>
<h3>{{ post.title }}</h3>
<span>{{ post.views }} views</span>
</article>
{% endfor %}
第三章:过滤器
内置过滤器
过滤器用于修改变量的输出,使用管道符 | 连接:
<!-- 字符串过滤器 -->
{{ name | upper }}
<!-- 大写 -->
{{ name | lower }}
<!-- 小写 -->
{{ name | capitalize }}
<!-- 首字母大写 -->
{{ name | title }}
<!-- 标题化 -->
{{ " hello " | trim }}
<!-- 去除空白 -->
{{ text | truncate(50) }}
<!-- 截断 -->
{{ text | truncate(50, true, '...') }} {{ text | wordwrap(79) }}
<!-- 自动换行 -->
{{ text | replace('old', 'new') }}
<!-- 替换 -->
{{ text | striptags }}
<!-- 去除 HTML 标签 -->
{{ text | center(80) }}
<!-- 居中填充 -->
<!-- HTML 过滤器 -->
{{ html_content | safe }}
<!-- 标记为安全(不转义) -->
{{ text | escape }}
<!-- HTML 转义 -->
{{ text | e }}
<!-- escape 的别名 -->
{{ text | urlize }}
<!-- URL 转链接 -->
<!-- 数字过滤器 -->
{{ 3.14159 | round(2) }}
<!-- 四舍五入: 3.14 -->
{{ 1234567 | filesizeformat }}
<!-- 文件大小格式化: 1.2 MB -->
{{ 42 | abs }}
<!-- 绝对值 -->
{{ items | length }}
<!-- 长度 -->
<!-- 列表过滤器 -->
{{ items | first }}
<!-- 第一个元素 -->
{{ items | last }}
<!-- 最后一个元素 -->
{{ items | length }}
<!-- 长度 -->
{{ items | reverse }}
<!-- 反转 -->
{{ items | sort }}
<!-- 排序 -->
{{ items | sort(attribute='name') }}
<!-- 按属性排序 -->
{{ items | sort(reverse=true) }}
<!-- 降序排序 -->
{{ items | unique }}
<!-- 去重 -->
{{ items | join(', ') }}
<!-- 连接为字符串 -->
{{ items | random }}
<!-- 随机元素 -->
{{ items | batch(3) }}
<!-- 分批 -->
{{ items | slice(3) }}
<!-- 切片 -->
{{ items | reject('none') }}
<!-- 过滤 None -->
{{ items | selectattr('active') }}
<!-- 按属性选择 -->
{{ items | rejectattr('deleted') }}
<!-- 按属性排除 -->
{{ items | map(attribute='name') }}
<!-- 提取属性 -->
{{ items | sum }}
<!-- 求和 -->
{{ items | sum(attribute='price') }}
<!-- 字典过滤器 -->
{{ dict | dictsort }}
<!-- 按键排序 -->
{{ dict | items }}
<!-- 转为列表 -->
<!-- 类型转换 -->
{{ value | int }}
<!-- 转整数 -->
{{ value | float }}
<!-- 转浮点数 -->
{{ value | string }}
<!-- 转字符串 -->
{{ value | list }}
<!-- 转列表 -->
<!-- 日期格式化(需要自定义或使用 babel) -->
{{ date | dateformat('%Y-%m-%d') }}
<!-- 默认值 -->
{{ value | default('N/A') }} {{ value | d('N/A') }}
<!-- default 的别名 -->
过滤器链
<!-- 多个过滤器可以链式使用 -->
{{ text | striptags | truncate(100) | capitalize }}
<!-- 带参数的过滤器链 -->
{{ items | sort(attribute='date', reverse=true) | first }}
<!-- 复杂示例 -->
{{ users | selectattr('active', 'equalto', true) | map(attribute='name') |
join(', ') }}
自定义过滤器
from flask import Flask
from datetime import datetime
app = Flask(__name__)
# 简单过滤器
@app.template_filter('reverse')
def reverse_filter(s):
return s[::-1]
# 带参数的过滤器
@app.template_filter('format_date')
def format_date(value, format='%Y-%m-%d'):
if isinstance(value, datetime):
return value.strftime(format)
return value
# 金额格式化
@app.template_filter('currency')
def currency_filter(value, symbol='¥'):
return f'{symbol}{value:,.2f}'
# 时间差计算
@app.template_filter('time_ago')
def time_ago_filter(value):
if not isinstance(value, datetime):
return value
now = datetime.now()
diff = now - value
if diff.days > 365:
return f'{diff.days // 365} 年前'
elif diff.days > 30:
return f'{diff.days // 30} 个月前'
elif diff.days > 0:
return f'{diff.days} 天前'
elif diff.seconds > 3600:
return f'{diff.seconds // 3600} 小时前'
elif diff.seconds > 60:
return f'{diff.seconds // 60} 分钟前'
else:
return '刚刚'
# 注册多个过滤器
def nl2br(value):
"""将换行符转换为 <br> 标签"""
return value.replace('\n', '<br>')
def highlight(value, query):
"""高亮搜索关键词"""
return value.replace(query, f'<mark>{query}</mark>')
app.jinja_env.filters['nl2br'] = nl2br
app.jinja_env.filters['highlight'] = highlight
<!-- 使用自定义过滤器 -->
{{ "hello" | reverse }}
<!-- olleh -->
{{ created_at | format_date('%Y年%m月%d日') }} {{ price | currency('$') }}
<!-- $1,234.56 -->
{{ post.created_at | time_ago }}
<!-- 2 小时前 -->
{{ description | nl2br | safe }} {{ content | highlight(query) | safe }}
第四章:控制结构
条件语句
<!-- 基本 if -->
{% if user %}
<h1>Hello, {{ user.name }}!</h1>
{% endif %}
<!-- if-else -->
{% if user.is_admin %}
<a href="/admin">Admin Panel</a>
{% else %}
<a href="/profile">Profile</a>
{% endif %}
<!-- if-elif-else -->
{% if user.role == 'admin' %}
<span class="badge admin">Admin</span>
{% elif user.role == 'moderator' %}
<span class="badge moderator">Moderator</span>
{% elif user.role == 'member' %}
<span class="badge member">Member</span>
{% else %}
<span class="badge guest">Guest</span>
{% endif %}
<!-- 内联条件 -->
<div class="{{ 'active' if is_active else 'inactive' }}">
{{ status if status else 'Unknown' }}
</div>
<!-- 复杂条件 -->
{% if user and user.is_active and 'admin' in user.roles %}
<p>Active admin user</p>
{% endif %} {% if items | length > 0 %}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% endif %}
循环语句
<!-- 基本循环 -->
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
<!-- 带索引的循环 -->
{% for item in items %}
<li>{{ loop.index }}. {{ item }}</li>
{% endfor %}
<!-- 循环变量 -->
{% for user in users %}
<div
class="user {{ 'first' if loop.first else '' }} {{ 'last' if loop.last else '' }}"
>
<span>索引(从1开始): {{ loop.index }}</span>
<span>索引(从0开始): {{ loop.index0 }}</span>
<span>反向索引: {{ loop.revindex }}</span>
<span>总数: {{ loop.length }}</span>
<span>是否第一个: {{ loop.first }}</span>
<span>是否最后一个: {{ loop.last }}</span>
<span>循环次数: {{ loop.cycle('odd', 'even') }}</span>
</div>
{% endfor %}
<!-- 空列表处理 -->
{% for item in items %}
<li>{{ item }}</li>
{% else %}
<li class="empty">No items found.</li>
{% endfor %}
<!-- 遍历字典 -->
{% for key, value in data.items() %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
<!-- 嵌套循环 -->
{% for category in categories %}
<h2>{{ category.name }}</h2>
<ul>
{% for product in category.products %}
<li>{{ product.name }} - {{ product.price }}</li>
{% endfor %}
</ul>
{% endfor %}
<!-- 访问父循环变量 -->
{% for category in categories %} {% for product in category.products %}
<p>
Category {{ loop.parent.loop.index }}: {{ category.name }}<br />
Product {{ loop.index }}: {{ product.name }}
</p>
{% endfor %} {% endfor %}
<!-- 循环过滤 -->
{% for user in users if user.is_active %}
<li>{{ user.name }}</li>
{% endfor %}
<!-- 递归循环 -->
{% for item in menu recursive %}
<li>
{{ item.name }} {% if item.children %}
<ul>
{{ loop(item.children) }}
</ul>
{% endif %}
</li>
{% endfor %}
循环变量参考
| 变量 | 说明 |
|---|---|
loop.index | 当前迭代次数(从 1 开始) |
loop.index0 | 当前迭代次数(从 0 开始) |
loop.revindex | 反向索引(从 1 开始) |
loop.revindex0 | 反向索引(从 0 开始) |
loop.first | 是否第一次迭代 |
loop.last | 是否最后一次迭代 |
loop.length | 序列长度 |
loop.cycle() | 循环使用参数值 |
loop.depth | 递归深度(从 1 开始) |
loop.depth0 | 递归深度(从 0 开始) |
loop.previtem | 上一个元素 |
loop.nextitem | 下一个元素 |
loop.changed() | 值是否改变 |
宏(Macros)
宏类似于函数,用于创建可重用的模板片段:
<!-- 定义宏 -->
{% macro input(name, value='', type='text', placeholder='') %}
<input
type="{{ type }}"
name="{{ name }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
class="form-control"
/>
{% endmacro %} {% macro button(text, type='button', class='btn-primary') %}
<button type="{{ type }}" class="btn {{ class }}">{{ text }}</button>
{% endmacro %}
<!-- 使用宏 -->
<form>
{{ input('username', placeholder='Enter username') }} {{ input('email',
type='email', placeholder='Enter email') }} {{ input('password',
type='password') }} {{ button('Submit', type='submit') }}
</form>
<!-- 带可选参数的宏 -->
{% macro render_field(field, label_visible=true) %}
<div class="form-group">
{% if label_visible %}
<label for="{{ field.id }}">{{ field.label.text }}</label>
{% endif %} {{ field(class='form-control') }} {% if field.errors %}
<ul class="errors">
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
<!-- 宏的特殊变量 -->
{% macro dialog(title) %}
<div class="dialog">
<h2>{{ title }}</h2>
{{ caller() }}
<!-- 调用传入的内容 -->
</div>
{% endmacro %} {% call dialog('Warning') %}
<p>Are you sure you want to delete this?</p>
<button>Yes</button>
<button>No</button>
{% endcall %}
<!-- 带参数的 call -->
{% macro list_items(items) %}
<ul>
{% for item in items %}
<li>{{ caller(item) }}</li>
{% endfor %}
</ul>
{% endmacro %} {% call(item) list_items(users) %}
<strong>{{ item.name }}</strong>: {{ item.email }} {% endcall %}
从外部文件导入宏
<!-- macros/forms.html -->
{% macro input(name, value='', type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" />
{% endmacro %} {% macro textarea(name, value='', rows=5) %}
<textarea name="{{ name }}" rows="{{ rows }}">{{ value }}</textarea>
{% endmacro %}
<!-- 在其他模板中导入 -->
{% from 'macros/forms.html' import input, textarea %}
<form>{{ input('title') }} {{ textarea('content', rows=10) }}</form>
<!-- 导入所有宏 -->
{% import 'macros/forms.html' as forms %}
<form>{{ forms.input('title') }} {{ forms.textarea('content') }}</form>
<!-- 带上下文导入 -->
{% from 'macros/forms.html' import input with context %}
第五章:模板继承
基础模板(Base Template)
<!-- templates/base.html -->
<!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 %}Default Title{% endblock %} - My Site</title>
<!-- 公共 CSS -->
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/base.css') }}"
/>
<!-- 页面特定 CSS -->
{% block styles %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
{% block navbar %}
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('profile') }}">Profile</a>
<a href="{{ url_for('logout') }}">Logout</a>
{% else %}
<a href="{{ url_for('login') }}">Login</a>
{% endif %} {% endblock %}
</nav>
<!-- 闪存消息 -->
{% 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 %}
<!-- 主内容区 -->
<main class="container">{% block content %}{% endblock %}</main>
<!-- 页脚 -->
<footer>
{% block footer %}
<p>© 2024 My Site. All rights reserved.</p>
{% endblock %}
</footer>
<!-- 公共 JS -->
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
<!-- 页面特定 JS -->
{% block scripts %}{% endblock %}
</body>
</html>
继承模板
<!-- templates/index.html -->
{% extends 'base.html' %} {% block title %}Home{% endblock %} {% block styles %}
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/home.css') }}"
/>
{% endblock %} {% block content %}
<div class="hero">
<h1>Welcome to My Site</h1>
<p>This is the home page.</p>
</div>
<section class="features">
{% for feature in features %}
<div class="feature-card">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
{% endfor %}
</section>
{% endblock %} {% block scripts %}
<script src="{{ url_for('static', filename='js/home.js') }}"></script>
{% endblock %}
多级继承
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
<!-- templates/layouts/admin.html -->
{% extends 'base.html' %} {% block body %}
<div class="admin-layout">
<aside class="sidebar">
{% block sidebar %}
<nav>
<a href="{{ url_for('admin.dashboard') }}">Dashboard</a>
<a href="{{ url_for('admin.users') }}">Users</a>
<a href="{{ url_for('admin.settings') }}">Settings</a>
</nav>
{% endblock %}
</aside>
<main class="admin-content">{% block content %}{% endblock %}</main>
</div>
{% endblock %}
<!-- templates/admin/users.html -->
{% extends 'layouts/admin.html' %} {% block title %}User Management{% endblock
%} {% block content %}
<h1>User Management</h1>
<table>
<!-- 用户列表 -->
</table>
{% endblock %}
super() 调用父块
<!-- templates/base.html -->
{% block sidebar %}
<nav class="sidebar">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
{% endblock %}
<!-- templates/admin.html -->
{% extends 'base.html' %} {% block sidebar %} {{ super() }}
<!-- 保留父模板内容 -->
<nav class="admin-nav">
<a href="/admin/dashboard">Dashboard</a>
<a href="/admin/users">Users</a>
</nav>
{% endblock %}
块的作用域
<!-- 块可以嵌套 -->
{% block outer %}
<div class="outer">
{% block inner %}
<p>Inner content</p>
{% endblock %}
</div>
{% endblock %}
<!-- 块可以重复使用(通过 self) -->
<title>{% block title %}{% endblock %}</title>
<h1>{{ self.title() }}</h1>
<!-- 块作为变量 -->
{% set navigation %}
<nav>
<a href="/">Home</a>
</nav>
{% endset %} {{ navigation }}
第六章:模板包含与导入
Include 包含
<!-- templates/partials/header.html -->
<header>
<h1>{{ site_name }}</h1>
<nav>
{% for item in menu_items %}
<a href="{{ item.url }}">{{ item.name }}</a>
{% endfor %}
</nav>
</header>
<!-- templates/partials/footer.html -->
<footer>
<p>© {{ current_year }} {{ site_name }}</p>
</footer>
<!-- templates/page.html -->
<!DOCTYPE html>
<html>
<body>
{% include 'partials/header.html' %}
<main>{% block content %}{% endblock %}</main>
{% include 'partials/footer.html' %}
</body>
</html>
<!-- 带上下文包含 -->
{% include 'partials/sidebar.html' with context %}
<!-- 忽略缺失的模板 -->
{% include 'partials/optional.html' ignore missing %}
<!-- 从列表中选择模板 -->
{% include ['partials/custom.html', 'partials/default.html'] %}
动态包含
# 视图函数
@app.route('/page/<page_name>')
def page(page_name):
return render_template(f'pages/{page_name}.html')
<!-- 动态包含组件 -->
{% for widget in widgets %} {% include 'widgets/' ~ widget.type ~ '.html' %} {%
endfor %}
第七章:测试(Tests)
内置测试
测试用于检查变量的特性,使用 is 关键字:
<!-- 值测试 -->
{% if value is defined %}...{% endif %} {% if value is undefined %}...{% endif
%} {% if value is none %}...{% endif %}
<!-- 类型测试 -->
{% if value is string %}String{% endif %} {% if value is number %}Number{% endif
%} {% if value is integer %}Integer{% endif %} {% if value is float %}Float{%
endif %} {% if value is mapping %}Dict{% endif %} {% if value is iterable
%}Iterable{% endif %} {% if value is sequence %}Sequence{% endif %} {% if value
is callable %}Callable{% endif %}
<!-- 比较测试 -->
{% if value is eq(other) %}Equal{% endif %} {% if value is ne(other) %}Not
Equal{% endif %} {% if value is lt(5) %}Less Than{% endif %} {% if value is
le(5) %}Less or Equal{% endif %} {% if value is gt(5) %}Greater Than{% endif %}
{% if value is ge(5) %}Greater or Equal{% endif %}
<!-- 字符串测试 -->
{% if name is lower %}Lowercase{% endif %} {% if name is upper %}Uppercase{%
endif %}
<!-- 可分性测试 -->
{% if loop.index is divisibleby(3) %}...{% endif %} {% if loop.index is odd
%}Odd{% endif %} {% if loop.index is even %}Even{% endif %}
<!-- 集合测试 -->
{% if items is sameas(other_items) %}Same Object{% endif %} {% if 'admin' is
in(roles) %}Has Admin{% endif %}
<!-- 否定测试 -->
{% if value is not defined %}Undefined{% endif %} {% if value is not none %}Has
Value{% endif %}
自定义测试
from flask import Flask
app = Flask(__name__)
# 注册自定义测试
@app.template_test('palindrome')
def is_palindrome(s):
return s == s[::-1]
@app.template_test('prime')
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@app.template_test('admin')
def is_admin(user):
return user and hasattr(user, 'role') and user.role == 'admin'
# 另一种注册方式
def is_weekend(date):
return date.weekday() >= 5
app.jinja_env.tests['weekend'] = is_weekend
<!-- 使用自定义测试 -->
{% if word is palindrome %} {{ word }} is a palindrome! {% endif %} {% if number
is prime %} {{ number }} is a prime number! {% endif %} {% if user is admin %}
<a href="/admin">Admin Panel</a>
{% endif %} {% if date is weekend %} It's weekend! {% endif %}
第八章:全局变量与函数
Flask 内置全局变量
<!-- 请求相关 -->
{{ request.method }} {{ request.path }} {{ request.args.get('page') }}
<!-- 会话 -->
{{ session.get('user_id') }}
<!-- 配置 -->
{{ config.DEBUG }} {{ config.SECRET_KEY }}
<!-- 闪存消息 -->
{% with messages = get_flashed_messages() %} {% for message in messages %}
<p>{{ message }}</p>
{% endfor %} {% endwith %}
<!-- URL 构建 -->
{{ url_for('static', filename='css/style.css') }} {{ url_for('user.profile',
username='john') }}
<!-- g 对象 -->
{{ g.user.name }}
自定义全局变量
from flask import Flask
from datetime import datetime
app = Flask(__name__)
# 添加全局变量
app.jinja_env.globals['site_name'] = 'My Flask Site'
app.jinja_env.globals['current_year'] = datetime.now().year
# 添加全局函数
@app.template_global()
def format_price(price, currency='USD'):
symbols = {'USD': '$', 'EUR': '€', 'CNY': '¥'}
return f"{symbols.get(currency, currency)}{price:,.2f}"
@app.template_global('now')
def get_now():
return datetime.now()
# 上下文处理器(为所有模板注入变量)
@app.context_processor
def inject_globals():
return {
'site_name': 'My Site',
'site_version': '1.0.0',
'menu_items': get_menu_items(),
'current_user': get_current_user(),
'debug_mode': app.debug
}
# 分离上下文处理器
def utility_processor():
def format_currency(amount, currency='CNY'):
return f'¥{amount:,.2f}' if currency == 'CNY' else f'${amount:,.2f}'
def gravatar_url(email, size=100):
import hashlib
hash = hashlib.md5(email.lower().encode()).hexdigest()
return f'https://www.gravatar.com/avatar/{hash}?s={size}'
return dict(
format_currency=format_currency,
gravatar_url=gravatar_url
)
app.context_processor(utility_processor)
<!-- 使用自定义全局变量和函数 -->
<h1>{{ site_name }}</h1>
<p>Version: {{ site_version }}</p>
<p>Price: {{ format_price(99.99, 'EUR') }}</p>
<p>Current time: {{ now() }}</p>
<img src="{{ gravatar_url(user.email) }}" alt="Avatar" />
第九章:转义与安全
自动转义
Jinja2 默认对 HTML 进行转义,防止 XSS 攻击:
# 视图函数
@app.route('/')
def index():
# 危险的用户输入
user_input = '<script>alert("XSS")</script>'
return render_template('index.html', content=user_input)
<!-- 自动转义(安全) -->
{{ content }}
<!-- 输出: <script>alert("XSS")</script> -->
<!-- 标记为安全(小心使用) -->
{{ content | safe }}
<!-- 输出: <script>alert("XSS")</script> -->
<!-- 显式转义 -->
{{ content | escape }} {{ content | e }}
手动控制转义
<!-- 关闭自动转义 -->
{% autoescape false %} {{ html_content }} {% endautoescape %}
<!-- 开启自动转义 -->
{% autoescape true %} {{ content }} {% endautoescape %}
<!-- 仅对特定部分转义 -->
{% autoescape false %}
<p>这里不转义: {{ trusted_html }}</p>
<p>这里转义: {{ user_input | e }}</p>
{% endautoescape %}
安全的 HTML 生成
from flask import Markup
from markupsafe import Markup, escape
# 使用 Markup 标记安全的 HTML
@app.template_global()
def icon(name):
return Markup(f'<i class="icon icon-{escape(name)}"></i>')
# 安全地构建 HTML
@app.template_global()
def render_link(url, text):
url = escape(url)
text = escape(text)
return Markup(f'<a href="{url}">{text}</a>')
# 处理用户输入
def format_user_bio(bio):
# 转义 HTML,但保留换行
bio = escape(bio)
bio = bio.replace('\n', Markup('<br>'))
return Markup(bio)
<!-- 使用安全函数 -->
{{ icon('home') }} {{ render_link('https://example.com', 'Example') }}
第十章:高级特性
空白控制
<!-- 默认保留空白 -->
{% for item in items %} {{ item }} {% endfor %}
<!-- 使用 - 去除空白 -->
{% for item in items -%} {{ item }} {%- endfor %}
<!-- 单独控制 -->
{%- if true -%}
<!-- 两边都去除空白 -->
{% if true -%}
<!-- 只去除右边空白 -->
{%- if true %}
<!-- 只去除左边空白 -->
<!-- 实际例子 -->
<ul>
{% for item in items -%}
<li>{{ item }}</li>
{% endfor -%}
</ul>
设置变量
<!-- 简单赋值 -->
{% set name = 'Flask' %} {% set count = 10 %}
<!-- 表达式赋值 -->
{% set full_name = first_name ~ ' ' ~ last_name %}
<!-- 块赋值 -->
{% set navigation %}
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
{% endset %} {{ navigation }}
<!-- 命名空间(在循环中修改变量) -->
{% set ns = namespace(count=0) %} {% for item in items %} {% set ns.count =
ns.count + 1 %} {% endfor %}
<p>Total: {{ ns.count }}</p>
原始输出
<!-- 输出原始 Jinja2 语法 -->
{% raw %}
<p>这是 Jinja2 语法示例:</p>
<code>{{ variable }}</code>
<code>{% for item in items %}</code>
{% endraw %}
With 语句
<!-- 创建局部作用域 -->
{% with %} {% set name = 'World' %}
<p>Hello, {{ name }}!</p>
{% endwith %}
<!-- name 在这里不可用 -->
<!-- 带参数的 with -->
{% with name='Flask', version='3.0' %}
<p>{{ name }} v{{ version }}</p>
{% endwith %}
<!-- 实用示例 -->
{% with messages = get_flashed_messages() %} {% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %} {% endwith %}
行语句
# 配置行语句前缀
app.jinja_env.line_statement_prefix = '#'
app.jinja_env.line_comment_prefix = '##'
# for item in items
<li>{{ item }}</li>
# endfor ## 这是注释 # if user
<p>Hello, {{ user.name }}</p>
# endif
沙箱模式
from jinja2.sandbox import SandboxedEnvironment
# 创建沙箱环境
env = SandboxedEnvironment()
# 在沙箱中渲染用户提供的模板
template = env.from_string(user_provided_template)
result = template.render(data=safe_data)
第十一章:性能优化
模板编译
from flask import Flask
app = Flask(__name__)
# 生产环境启用字节码缓存
app.config['TEMPLATES_AUTO_RELOAD'] = False # 禁用自动重载
# 使用文件系统缓存
from jinja2 import FileSystemBytecodeCache
app.jinja_env.bytecode_cache = FileSystemBytecodeCache('/tmp/jinja_cache')
# 或使用内存缓存
from jinja2 import MemcachedBytecodeCache
app.jinja_env.bytecode_cache = MemcachedBytecodeCache(client)
优化技巧
<!-- 1. 避免重复计算 -->
<!-- 不好 -->
{% for item in expensive_query() %} {{ item }} {% endfor %}
<!-- 好 -->
{% set items = expensive_query() %} {% for item in items %} {{ item }} {% endfor
%}
<!-- 2. 使用 selectattr/rejectattr 而不是条件判断 -->
<!-- 不好 -->
{% for user in users %} {% if user.active %} {{ user.name }} {% endif %} {%
endfor %}
<!-- 好 -->
{% for user in users | selectattr('active') %} {{ user.name }} {% endfor %}
<!-- 3. 预先过滤数据 -->
<!-- 在视图函数中过滤 -->
@app.route('/users') def users(): active_users =
User.query.filter_by(active=True).all() return render_template('users.html',
users=active_users)
<!-- 4. 使用 include 拆分复杂模板 -->
{% include 'partials/user_card.html' %}
<!-- 5. 避免过深的嵌套 -->
第十二章:实战示例
分页组件
<!-- templates/macros/pagination.html -->
{% macro render_pagination(pagination, endpoint) %}
<nav class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for(endpoint, page=pagination.prev_num) }}">« Prev</a>
{% else %}
<span class="disabled">« Prev</span>
{% endif %} {% for page in pagination.iter_pages() %} {% if page %} {% if page
== pagination.page %}
<span class="current">{{ page }}</span>
{% else %}
<a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
{% endif %} {% else %}
<span class="ellipsis">...</span>
{% endif %} {% endfor %} {% if pagination.has_next %}
<a href="{{ url_for(endpoint, page=pagination.next_num) }}">Next »</a>
{% else %}
<span class="disabled">Next »</span>
{% endif %}
</nav>
{% endmacro %}
表单组件
<!-- templates/macros/forms.html -->
{% macro form_field(field, label=true, placeholder='') %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
{% if label %} {{ field.label(class='form-label') }} {% endif %} {% if
field.type == 'TextAreaField' %} {{ field(class='form-control',
placeholder=placeholder, rows=5) }} {% elif field.type == 'SelectField' %} {{
field(class='form-select') }} {% elif field.type == 'BooleanField' %}
<div class="form-check">
{{ field(class='form-check-input') }} {{
field.label(class='form-check-label') }}
</div>
{% else %} {{ field(class='form-control', placeholder=placeholder) }} {% endif
%} {% if field.description %}
<small class="form-text text-muted">{{ field.description }}</small>
{% endif %} {% if field.errors %}
<ul class="error-list">
{% for error in field.errors %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %} {% macro form_submit(text='Submit', class='btn-primary') %}
<div class="form-group">
<button type="submit" class="btn {{ class }}">{{ text }}</button>
</div>
{% endmacro %}
博客文章卡片
<!-- templates/macros/blog.html -->
{% macro article_card(post, show_content=true) %}
<article class="blog-card">
{% if post.cover_image %}
<img src="{{ post.cover_image }}" alt="{{ post.title }}" class="card-image" />
{% endif %}
<div class="card-body">
<header>
<h2>
<a href="{{ url_for('post.detail', slug=post.slug) }}"
>{{ post.title }}</a
>
</h2>
<div class="meta">
<span class="author">
<img src="{{ gravatar_url(post.author.email, 32) }}" alt="" />
{{ post.author.name }}
</span>
<time datetime="{{ post.created_at.isoformat() }}">
{{ post.created_at | time_ago }}
</time>
<span class="category">{{ post.category.name }}</span>
</div>
</header>
{% if show_content %}
<div class="excerpt">{{ post.content | striptags | truncate(200) }}</div>
{% endif %}
<footer>
<div class="tags">
{% for tag in post.tags %}
<a href="{{ url_for('tag.posts', slug=tag.slug) }}" class="tag">
{{ tag.name }}
</a>
{% endfor %}
</div>
<a href="{{ url_for('post.detail', slug=post.slug) }}" class="read-more">
Read More →
</a>
</footer>
</div>
</article>
{% endmacro %}
总结
本章详细介绍了 Jinja2 模板引擎:
- 基础语法:变量输出、表达式、注释
- 过滤器:内置过滤器、自定义过滤器、过滤器链
- 控制结构:条件、循环、宏
- 模板继承:基础模板、块、super()
- 包含与导入:include、import、动态包含
- 测试:内置测试、自定义测试
- 全局变量:Flask 内置变量、自定义变量、上下文处理器
- 安全:自动转义、手动控制、Markup
- 高级特性:空白控制、变量设置、沙箱模式
- 性能优化:模板编译、缓存、优化技巧
- 实战示例:分页、表单、博客组件
下一章我们将学习表单处理与数据验证。