共计 14828 个字符,预计需要花费 38 分钟才能阅读完成。
目录
Jinja2 简介
Jinja2 是一个现代的、设计友好的 Python 模板引擎,它是 Flask 框架的默认模板引擎。Jinja2 模板允许你在 HTML 中嵌入动态内容,使用类似 Python 的语法。
主要特性
- 安全的 HTML 转义:自动转义 HTML 特殊字符,防止 XSS 攻击
- 模板继承:支持模板的继承和块重写
- 强大的过滤器系统:内置丰富的过滤器,支持自定义过滤器
- 宏系统:类似函数的功能,可重用模板代码
- 高性能:编译模板为 Python 字节码,执行效率高
基础语法
变量输出
{# 基本变量输出 #}
<h1>Hello, {{name}}!</h1>
<p>Your age is: {{age}}</p>
{# 对象属性访问 #}
<p>User: {{user.username}}</p>
<p>Email: {{user.email}}</p>
{# 字典访问 #}
<p>Value: {{my_dict.key}}</p>
<p>Value: {{my_dict['key'] }}</p>
{# 列表访问 #}
<p>First item: {{my_list[0] }}</p>
{# 方法调用 #}
<p>Length: {{my_string.upper() }}</p>
注释
{# 这是单行注释 #}
{#
这是
多行
注释
#}
表达式
{# 数学运算 #}
<p>Total: {{price * quantity}}</p>
<p>Average: {{(a + b) / 2 }}</p>
{# 字符串连接 #}
<p>{{"Hello" + name + "!"}}</p>
{# 比较运算 #}
{% if count > 10 %}
<p>More than 10 items</p>
{% endif %}
{# 逻辑运算 #}
{% if user and user.is_active %}
<p>Active user</p>
{% endif %}
控制结构
条件语句
{# 基本 if 语句 #}
{% if user %}
<p>Hello, {{user.name}}!</p>
{% endif %}
{# if-else 语句 #}
{% if temperature > 30 %}
<p>It's hot outside</p>
{% else %}
<p>It's not too hot</p>
{% endif %}
{# if-elif-else 语句 #}
{% if score >= 90 %}
<p>Grade: A</p>
{% elif score >= 80 %}
<p>Grade: B</p>
{% elif score >= 70 %}
<p>Grade: C</p>
{% else %}
<p>Grade: F</p>
{% endif %}
{# 复杂条件 #}
{% if user.role in ['admin', 'moderator'] and user.is_active %}
<p>You have special privileges</p>
{% endif %}
循环语句
{# 基本 for 循环 #}
<ul>
{% for item in items %}
<li>{{item}}</li>
{% endfor %}
</ul>
{# 带索引的循环 #}
<table>
{% for user in users %}
<tr>
<td>{{loop.index}}</td>
<td>{{user.name}}</td>
<td>{{user.email}}</td>
</tr>
{% endfor %}
</table>
{# 循环控制变量 #}
<ul>
{% for item in items %}
<li class="{% if loop.first %}first{% endif %} {% if loop.last %}last{% endif %}">
{{item}}
{% if not loop.last %},{% endif %}
</li>
{% endfor %}
</ul>
{# 循环字典 #}
<dl>
{% for key, value in my_dict.items() %}
<dt>{{key}}</dt>
<dd>{{value}}</dd>
{% endfor %}
</dl>
{# 空循环处理 #}
{% for user in users %}
<p>{{user.name}}</p>
{% else %}
<p>No users found.</p>
{% endfor %}
{# 循环中断和继续 #}
{% for number in numbers %}
{% if number > 100 %}
{% break %}
{% endif %}
{% if number % 2 == 0 %}
{% continue %}
{% endif %}
<p>{{number}}</p>
{% endfor %}
循环控制变量
| 变量 | 描述 |
|---|---|
loop.index |
当前迭代的索引(从 1 开始) |
loop.index0 |
当前迭代的索引(从 0 开始) |
loop.revindex |
反向迭代的索引(从 1 开始) |
loop.revindex0 |
反向迭代的索引(从 0 开始) |
loop.first |
如果是第一次迭代,则为 True |
loop.last |
如果是最后一次迭代,则为 True |
loop.length |
序列中的项目数量 |
loop.cycle |
在序列值之间循环的辅助函数 |
loop.depth |
当前在递归循环中的深度 |
loop.depth0 |
当前在递归循环中的深度(从 0 开始) |
过滤器
内置过滤器
{# 字符串过滤器 #}
<p>{{text|upper}}</p>
<p>{{text|lower}}</p>
<p>{{text|title}}</p>
<p>{{text|capitalize}}</p>
<p>{{text|trim}}</p>
<p>{{text|replace('old', 'new') }}</p>
<p>{{text|truncate(50) }}</p>
{# 数字过滤器 #}
<p>{{number|round(2) }}</p>
<p>{{number|abs}}</p>
<p>{{percentage|format('0.2%') }}</p>
{# 列表过滤器 #}
<p>{{list|length}}</p>
<p>{{list|first}}</p>
<p>{{list|last}}</p>
<p>{{list|sort}}</p>
<p>{{list|join(',') }}</p>
{# 日期过滤器 #}
<p>{{date|datetimeformat}}</p>
<p>{{date|datetimeformat('%Y-%m-%d') }}</p>
{# 默认值过滤器 #}
<p>{{value|default('N/A') }}</p>
<p>{{value|default('N/A', true) }}</p> {# 对于 false 值也使用默认值 #}
{# 安全过滤器 #}
<p>{{html_content|safe}}</p> {# 标记为安全 HTML #}
<p>{{text|escape}}</p> {# HTML 转义 #}
<p>{{text|e}}</p> {# escape 的简写 #}
{# 链式过滤器 #}
<p>{{text|trim|upper|truncate(100) }}</p>
常用过滤器示例
{# 格式化货币 #}
<p>Price: {{price|format_currency}}</p>
{# 相对时间 #}
<p>Posted {{post_date|timesince}}</p>
{# URL 编码 #}
<a href="/search?q={{query|urlencode}}">Search</a>
{# 单词计数 #}
<p>{{text|wordcount}} words</p>
{# 随机排序 #}
<ul>
{% for item in items|shuffle %}
<li>{{item}}</li>
{% endfor %}
</ul>
{# 分组操作 #}
{% for group in users|groupby('department') %}
<h3>{{group.grouper}}</h3>
<ul>
{% for user in group.list %}
<li>{{user.name}}</li>
{% endfor %}
</ul>
{% endfor %}
模板继承
基础模板 (base.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Website{% endblock %}</title>
{# CSS 块 #}
{% block styles %}
<link rel="stylesheet" href="{{url_for('static', filename='css/main.css') }}">
{% endblock %}
{# 额外的头部内容 #}
{% block head_extra %}{% endblock %}
</head>
<body>
{# 导航栏 #}
{% block navbar %}
<nav class="navbar">
<div class="nav-container">
<a href="{{url_for('index') }}" class="nav-logo">MySite</a>
<ul class="nav-menu">
<li><a href="{{url_for('about') }}">About</a></li>
<li><a href="{{url_for('contact') }}">Contact</a></li>
</ul>
</div>
</nav>
{% endblock %}
{# 主要内容区域 #}
<main class="container">
{# 闪存消息 #}
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash-messages">
{% for message in messages %}
<div class="flash-message">{{message}}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{# 页面内容 #}
{% block content %}{% endblock %}
</main>
{# 页脚 #}
{% block footer %}
<footer class="footer">
<p>© 2024 My Website. All rights reserved.</p>
</footer>
{% endblock %}
{# JavaScript 块 #}
{% block scripts %}
<script src="{{url_for('static', filename='js/main.js') }}"></script>
{% endblock %}
{# 额外的脚本 #}
{% block scripts_extra %}{% endblock %}
</body>
</html>
子模板使用
{# 继承基础模板 #}
{% extends "base.html" %}
{# 设置页面标题 #}
{% block title %}Home Page - My Website{% endblock %}
{# 添加额外的 CSS #}
{% block styles %}
{{super() }} {# 保留父模板的样式 #}
<link rel="stylesheet" href="{{url_for('static', filename='css/home.css') }}">
{% endblock %}
{# 重写导航栏 #}
{% block navbar %}
<nav class="navbar home-navbar">
<div class="nav-container">
<a href="{{url_for('index') }}" class="nav-logo">HomeSite</a>
<ul class="nav-menu">
<li><a href="{{url_for('index') }}">Home</a></li>
<li><a href="{{url_for('products') }}">Products</a></li>
<li><a href="{{url_for('about') }}">About</a></li>
</ul>
</div>
</nav>
{% endblock %}
{# 主要内容 #}
{% block content %}
<div class="hero-section">
<h1>Welcome to Our Website</h1>
<p>This is the home page content.</p>
{# 使用宏 #}
{{macros.render_feature_cards(features) }}
</div>
<div class="content-section">
<h2>Latest News</h2>
{% for news_item in news %}
<article class="news-article">
<h3>{{news_item.title}}</h3>
<p class="meta">Posted on {{news_item.date|datetimeformat}}</p>
<p>{{news_item.content|truncate(200) }}</p>
<a href="{{url_for('news_detail', id=news_item.id) }}">Read more</a>
</article>
{% else %}
<p>No news available at the moment.</p>
{% endfor %}
</div>
{% endblock %}
{# 添加页面特定的 JavaScript #}
{% block scripts_extra %}
<script>
// 首页特定的 JavaScript 代码
document.addEventListener('DOMContentLoaded', function() {console.log('Home page loaded');
});
</script>
{% endblock %}
宏
定义和使用宏
{# 在单独的宏文件中定义 #}
{# macros.html #}
{% macro render_form_field(field, class='form-control') %}
<div class="form-group">
<label for="{{field.id}}">{{field.label.text}}</label>
{{field(class=class, **kwargs) }}
{% if field.errors %}
<div class="errors">
{% for error in field.errors %}
<span class="error">{{error}}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro render_button(text, type='button', class='btn btn-primary') %}
<button type="{{type}}" class="{{class}}">{{text}}</button>
{% endmacro %}
{% macro render_card(title, content, footer=None) %}
<div class="card">
<div class="card-header">
<h3>{{title}}</h3>
</div>
<div class="card-body">
{{content}}
</div>
{% if footer %}
<div class="card-footer">
{{footer}}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro render_pagination(pagination, endpoint) %}
{% if pagination.pages > 1 %}
<nav class="pagination">
<ul>
{% if pagination.has_prev %}
<li><a href="{{url_for(endpoint, page=pagination.prev_num) }}">Previous</a></li>
{% endif %}
{% for page in pagination.iter_pages() %}
{% if page %}
<li class="{% if page == pagination.page %}active{% endif %}">
<a href="{{url_for(endpoint, page=page) }}">{{page}}</a>
</li>
{% else %}
<li class="disabled"><span>…</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li><a href="{{url_for(endpoint, page=pagination.next_num) }}">Next</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endmacro %}
在模板中使用宏
{# 导入宏文件 #}
{% from "macros.html" import render_form_field, render_button, render_card, render_pagination %}
{# 使用表单字段宏 #}
<form method="POST">
{{form.hidden_tag() }}
{{render_form_field(form.username) }}
{{render_form_field(form.email) }}
{{render_form_field(form.password) }}
{{render_button('Submit', type='submit') }}
</form>
{# 使用卡片宏 #}
<div class="card-grid">
{{ render_card(
"Feature One",
"This is the description of feature one.",
footer=render_button('Learn More')
) }}
{{ render_card(
"Feature Two",
"This is the description of feature two."
) }}
</div>
{# 使用分页宏 #}
{{render_pagination(posts, 'blog.index') }}
包含
使用包含组织模板
{# header.html #}
<header class="site-header">
<div class="container">
<div class="logo">
<a href="{{url_for('index') }}">
<img src="{{url_for('static', filename='images/logo.png') }}" alt="Logo">
</a>
</div>
<nav class="main-nav">
<ul>
<li><a href="{{url_for('index') }}">Home</a></li>
<li><a href="{{url_for('about') }}">About</a></li>
<li><a href="{{url_for('services') }}">Services</a></li>
<li><a href="{{url_for('contact') }}">Contact</a></li>
</ul>
</nav>
</div>
</header>
{# footer.html #}
<footer class="site-footer">
<div class="container">
<div class="footer-content">
<div class="footer-section">
<h4>Company</h4>
<ul>
<li><a href="{{url_for('about') }}">About Us</a></li>
<li><a href="{{url_for('careers') }}">Careers</a></li>
<li><a href="{{url_for('contact') }}">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Resources</h4>
<ul>
<li><a href="{{url_for('blog') }}">Blog</a></li>
<li><a href="{{url_for('docs') }}">Documentation</a></li>
<li><a href="{{url_for('support') }}">Support</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>© 2024 My Company. All rights reserved.</p>
</div>
</div>
</footer>
{# 在主模板中使用包含 #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
{% include 'header.html' %}
<main>
{% block content %}{% endblock %}
</main>
{% include 'footer.html' %}
</body>
</html>
高级特性
自定义过滤器
# 在 Flask 应用中注册自定义过滤器
from flask import Flask
import jinja2
app = Flask(__name__)
@app.template_filter('datetimeformat')
def datetimeformat(value, format='%Y-%m-%d %H:%M:%S'):
if value is None:
return ""
return value.strftime(format)
@app.template_filter('format_currency')
def format_currency(value):
if value is None:
return "$0.00"
return f"${value:,.2f}"
@app.template_filter('timesince')
def timesince(dt):
now = datetime.now()
diff = now - dt
if diff.days > 365:
years = diff.days // 365
return f"{years} year{'s'if years > 1 else''} ago"
elif diff.days > 30:
months = diff.days // 30
return f"{months} month{'s'if months > 1 else''} ago"
elif diff.days > 0:
return f"{diff.days} day{'s'if diff.days > 1 else''} ago"
elif diff.seconds > 3600:
hours = diff.seconds // 3600
return f"{hours} hour{'s'if hours > 1 else''} ago"
elif diff.seconds > 60:
minutes = diff.seconds // 60
return f"{minutes} minute{'s'if minutes > 1 else''} ago"
else:
return "just now"
在模板中使用自定义过滤器
{# 使用自定义过滤器 #}
<p>Published: {{post.created_at|datetimeformat}}</p>
<p>Price: {{product.price|format_currency}}</p>
<p>Last active: {{user.last_seen|timesince}}</p>
上下文处理器
# 添加全局变量到所有模板上下文
@app.context_processor
def inject_global_variables():
return {
'site_name': 'My Awesome Site',
'current_year': datetime.now().year,
'config': app.config
}
@app.context_processor
def utility_processor():
def format_phone(number):
return f"({number[:3]}) {number[3:6]}-{number[6:]}"
return dict(format_phone=format_phone)
在模板中使用上下文变量
<footer>
<p>© {{current_year}} {{site_name}}</p>
<p>Contact: {{format_phone('1234567890') }}</p>
</footer>
在 Flask 中使用 Jinja2
基本配置
from flask import Flask, render_template, url_for
app = Flask(__name__)
# 自定义模板过滤器
@app.template_filter('reverse')
def reverse_filter(s):
return s[::-1]
# 上下文处理器
@app.context_processor
def utility_processor():
def format_price(amount):
return f"${amount:.2f}"
return dict(format_price=format_price)
# 路由和视图
@app.route('/')
def index():
return render_template('index.html',
title='Home Page',
user={'name': 'John', 'role': 'admin'})
@app.route('/users')
def users():
users = [
{'name': 'Alice', 'email': 'alice@example.com'},
{'name': 'Bob', 'email': 'bob@example.com'},
{'name': 'Charlie', 'email': 'charlie@example.com'}
]
return render_template('users.html', users=users)
@app.route('/products')
def products():
products = [
{'name': 'Product 1', 'price': 19.99, 'in_stock': True},
{'name': 'Product 2', 'price': 29.99, 'in_stock': False},
{'name': 'Product 3', 'price': 39.99, 'in_stock': True}
]
return render_template('products.html', products=products)
完整的 Flask 应用示例
# app.py
from flask import Flask, render_template, request, flash, redirect, url_for
from datetime import datetime
import os
app = Flask(__name__)
app.secret_key = 'your-secret-key-here'
# 示例数据
blog_posts = [
{
'id': 1,
'title': 'First Blog Post',
'content': 'This is the content of the first blog post.',
'author': 'John Doe',
'created_at': datetime(2024, 1, 15),
'category': 'General'
},
{
'id': 2,
'title': 'Second Blog Post',
'content': 'This is the content of the second blog post.',
'author': 'Jane Smith',
'created_at': datetime(2024, 1, 20),
'category': 'Technology'
}
]
@app.route('/')
def index():
return render_template('index.html',
posts=blog_posts[:3], # 最新 3 篇文章
featured=True)
@app.route('/blog')
def blog():
category = request.args.get('category')
if category:
filtered_posts = [p for p in blog_posts if p['category'] == category]
else:
filtered_posts = blog_posts
return render_template('blog.html',
posts=filtered_posts,
categories=set(p['category'] for p in blog_posts))
@app.route('/post/<int:post_id>')
def post_detail(post_id):
post = next((p for p in blog_posts if p['id'] == post_id), None)
if not post:
flash('Post not found!', 'error')
return redirect(url_for('blog'))
return render_template('post_detail.html', post=post)
# 自定义过滤器
@app.template_filter('excerpt')
def excerpt_filter(text, length=100):
if len(text) <= length:
return text
return text[:length] + '...'
# 上下文处理器
@app.context_processor
def inject_now():
return {'now': datetime.now()}
if __name__ == '__main__':
app.run(debug=True)
对应的模板文件
{# templates/blog.html #}
{% extends "base.html" %}
{% block title %}Blog - My Site{% endblock %}
{% block content %}
<div class="blog-container">
<h1>Blog Posts</h1>
{# 分类筛选 #}
<div class="category-filter">
<strong>Filter by category:</strong>
<a href="{{url_for('blog') }}" class="{% if not request.args.get('category') %}active{% endif %}">All</a>
{% for category in categories %}
<a href="{{url_for('blog', category=category) }}"
class="{% if request.args.get('category') == category %}active{% endif %}">
{{category}}
</a>
{% endfor %}
</div>
{# 文章列表 #}
<div class="posts-grid">
{% for post in posts %}
<article class="post-card">
<h2><a href="{{url_for('post_detail', post_id=post.id) }}">{{post.title}}</a></h2>
<div class="post-meta">
<span>By {{post.author}}</span>
<span>on {{post.created_at|datetimeformat('%B %d, %Y') }}</span>
<span class="category">{{post.category}}</span>
</div>
<p>{{post.content|excerpt(150) }}</p>
<a href="{{url_for('post_detail', post_id=post.id) }}" class="read-more">Read more</a>
</article>
{% else %}
<p>No posts found.</p>
{% endfor %}
</div>
</div>
{% endblock %}
最佳实践
1. 模板组织
templates/
├── base.html # 基础模板
├── includes/ # 包含片段
│ ├── header.html
│ ├── footer.html
│ ├── navigation.html
│ └── sidebar.html
├── macros/ # 宏文件
│ ├── forms.html
│ ├── ui.html
│ └── pagination.html
├── auth/ # 认证相关模板
│ ├── login.html
│ └── register.html
├── blog/ # 博客相关模板
│ ├── blog.html
│ ├── post_detail.html
│ └── post_form.html
└── errors/ # 错误页面
├── 404.html
└── 500.html
2. 安全最佳实践
{# 始终对用户输入进行转义 #}
<p>{{user_input|e}}</p>
{# 谨慎使用 safe 过滤器 #}
{% if user.is_trusted %}
{{user_content|safe}}
{% else %}
{{user_content|e}}
{% endif %}
{# 使用 url_for 生成 URL,避免硬编码 #}
<a href="{{url_for('user_profile', username=user.username) }}">Profile</a>
{# 不要在模板中执行敏感操作 #}
{# 错误示例 #}
{% if user.password == 'secret' %} {# 不要这样做!#}
3. 性能优化
{# 使用循环优化 #}
{% for item in items %}
{# 在循环外计算固定值 #}
{% set item_class = 'even' if loop.index0 % 2 == 0 else 'odd' %}
<div class="{{item_class}}">{{item}}</div>
{% endfor %}
{# 避免在循环中进行复杂计算 #}
{# 错误示例 #}
{% for user in users %}
<p>{{calculate_user_score(user) }}</p> {# 复杂计算应在视图函数中完成 #}
{% endfor %}
{# 使用缓存 #}
{% cache 300, 'recent_posts' %}
{# 这部分内容将被缓存 5 分钟 #}
{% for post in recent_posts %}
{{render_post_summary(post) }}
{% endfor %}
{% endcache %}
4. 可维护性技巧
{# 使用有意义的块名称 #}
{% block page_styles %}{% endblock %}
{% block page_scripts %}{% endblock %}
{% block sidebar_content %}{% endblock %}
{# 保持模板简洁 #}
{# 将复杂逻辑移到宏或视图函数中 #}
{# 使用注释说明复杂的模板逻辑 #}
{#
这个循环生成一个分页导航
参数:
- pagination: 分页对象
- endpoint: 视图端点名称
#}
{{render_pagination(pagination, endpoint) }}
{# 一致的缩进和格式 #}
{% if condition %}
<div>
<p>Content</p>
</div>
{% endif %}
正文完

