模板引擎¶
📖 章节简介¶
本章将介绍Flask的模板引擎Jinja2,学习如何使用模板渲染HTML页面,实现页面内容的动态生成和复用。
🎨 Jinja2基础¶
1. 模板语法¶
变量输出¶
HTML
<!-- 输出变量 -->
{{ variable }}
<!-- 输出字典值 -->
{{ user.name }}
{{ user['email'] }}
<!-- 输出列表元素 -->
{{ items[0] }}
<!-- 输出对象属性 -->
{{ user.get_name() }}
<!-- 带默认值的输出 -->
{{ user.name or '匿名用户' }}
<!-- 过滤器 -->
{{ name|capitalize }}
{{ price|float }}
{{ text|striptags }}
{# strftime 非内置过滤器,需通过 @app.template_filter() 自定义注册 #}
控制结构¶
HTML
<!-- if语句 -->
{% if user %}
<p>欢迎, {{ user.name }}!</p>
{% elif guest %}
<p>欢迎, 访客!</p>
{% else %}
<p>请先登录</p>
{% endif %}
<!-- for循环 -->
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
<!-- 带else的for循环 -->
{% for item in items %}
<li>{{ item.name }}</li>
{% else %}
<li>没有项目</li>
{% endfor %}
<!-- 循环变量 -->
{% for user in users %}
<p>{{ loop.index }}: {{ user.name }}</p>
<p>当前索引: {{ loop.index0 }}</p>
<p>是否第一次: {{ loop.first }}</p>
<p>是否最后一次: {{ loop.last }}</p>
{% endfor %}
2. 过滤器¶
常用过滤器¶
HTML
<!-- 字符串过滤器 -->
{{ text|upper }} <!-- 转大写 -->
{{ text|lower }} <!-- 转小写 -->
{{ text|capitalize }} <!-- 首字母大写 -->
{{ text|title }} <!-- 标题格式 -->
{{ text|trim }} <!-- 去除首尾空格 -->
{{ text|striptags }} <!-- 去除HTML标签 -->
<!-- 数字过滤器 -->
{{ number|round }} <!-- 四舍五入 -->
{{ number|round(2) }} <!-- 保留两位小数 -->
{{ number|int }} <!-- 转整数 -->
{{ number|float }} <!-- 转浮点数 -->
<!-- 列表过滤器 -->
{{ items|length }} <!-- 列表长度 -->
{{ items|first }} <!-- 第一个元素 -->
{{ items|last }} <!-- 最后一个元素 -->
{{ items|sort }} <!-- 排序 -->
{{ items|reverse }} <!-- 反转 -->
<!-- 安全过滤器 -->
{{ user_input|safe }} <!-- 不转义HTML,⚠️ 有XSS风险!必须先对内容进行消毒(如用bleach库)再使用|safe -->
{{ user_input|escape }} <!-- 转义HTML -->
{{ user_input|e }} <!-- escape的简写 -->
<!-- 日期过滤器(需自定义注册,非Jinja2内置) -->
{# 示例用法(需先通过 @app.template_filter() 注册) #}
{{ date|datetime('%Y年%m月%d日') }}
自定义过滤器¶
Python
# 在Flask应用中注册自定义过滤器
from flask import Flask
app = Flask(__name__)
@app.template_filter('reverse')
def reverse_filter(s):
"""反转字符串"""
return s[::-1] # 切片操作:[start:end:step]提取子序列
@app.template_filter('currency')
def currency_filter(value):
"""格式化为货币"""
return f'¥{value:,.2f}'
@app.template_filter('truncate_words')
def truncate_words_filter(s, num=10):
"""截断文本为指定单词数"""
words = s.split()
if len(words) > num:
return ' '.join(words[:num]) + '...'
return s
🧩 模板继承¶
1. 基础模板¶
HTML
<!-- 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 %}我的网站{% endblock %}</title>
<!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="container">
<a href="{{ url_for('main.index') }}" class="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>
<li><a href="{{ url_for('auth.register') }}">注册</a></li>
{% endif %}
</ul>
</div>
</nav>
<!-- 消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- 主内容区 -->
<main class="main-content">
{% block content %}{% endblock %}
</main>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>© {{ now.year }} 我的网站. 保留所有权利.</p>
</div>
</footer>
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
2. 子模板¶
HTML
<!-- templates/index.html -->
{% extends "base.html" %}
{% block title %}首页 - 我的网站{% endblock %}
{% block content %}
<div class="hero">
<h1>欢迎来到我的网站</h1>
<p>这是一个使用Flask构建的网站</p>
<a href="{{ url_for('main.about') }}" class="btn">了解更多</a>
</div>
<div class="features">
<h2>我们的特色</h2>
<div class="feature-grid">
{% for feature in features %}
<div class="feature-card">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
HTML
<!-- templates/user/profile.html -->
{% extends "base.html" %}
{% block title %}个人资料 - {{ user.name }}{% endblock %}
{% block content %}
<div class="profile">
<div class="profile-header">
<img src="{{ user.avatar_url or url_for('static', filename='images/default-avatar.png') }}"
alt="{{ user.name }}"
class="avatar">
<h1>{{ user.name }}</h1>
<p class="bio">{{ user.bio or '这个人很懒,什么都没写' }}</p>
</div>
<div class="profile-stats">
<div class="stat">
<span class="stat-value">{{ user.posts|length }}</span>
<span class="stat-label">文章</span>
</div>
<div class="stat">
<span class="stat-value">{{ user.followers|length }}</span>
<span class="stat-label">关注者</span>
</div>
<div class="stat">
<span class="stat-value">{{ user.following|length }}</span>
<span class="stat-label">关注中</span>
</div>
</div>
<div class="profile-actions">
{% if current_user.id != user.id %}
<button class="btn btn-primary">关注</button>
<button class="btn btn-secondary">私信</button>
{% else %}
<a href="{{ url_for('user.edit_profile') }}" class="btn">编辑资料</a>
{% endif %}
</div>
</div>
{% endblock %}
🔄 模板包含¶
1. 包含模板¶
HTML
<!-- templates/components/navbar.html -->
<nav class="navbar">
<div class="container">
<a href="{{ url_for('main.index') }}" class="logo">我的网站</a>
<ul class="nav-links">
<li><a href="{{ url_for('main.index') }}">首页</a></li>
<li><a href="{{ url_for('main.about') }}">关于</a></li>
<li><a href="{{ url_for('main.contact') }}">联系</a></li>
</ul>
</div>
</nav>
2. 宏(Macro)¶
HTML
<!-- templates/macros/form.html -->
{% macro render_field(field) %}
<div class="form-group">
{{ field.label(class='form-label') }}
{{ field(class='form-control') }}
{% if field.errors %}
<div class="invalid-feedback">
{% for error in field.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro render_button(text, type='primary') %}
<button type="submit" class="btn btn-{{ type }}">{{ text }}</button>
{% endmacro %}
{% macro render_pagination(pagination, endpoint) %}
<nav aria-label="分页">
<ul class="pagination">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.prev_num) }}">上一页</a>
</li>
{% endif %}
{% for page in pagination.iter_pages() %}
{% if page %}
<li class="page-item {% if page == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for(endpoint, page=pagination.next_num) }}">下一页</a>
</li>
{% endif %}
</ul>
</nav>
{% endmacro %}
HTML
<!-- 使用宏 -->
{% from 'macros/form.html' import render_field, render_button, render_pagination %}
<form method="POST">
{{ form.hidden_tag() }}
{{ render_field(form.username) }}
{{ render_field(form.email) }}
{{ render_field(form.password) }}
{{ render_button('登录') }}
</form>
<!-- 使用分页宏 -->
{{ render_pagination(pagination, 'main.index') }}
🌐 全局函数¶
1. 内置全局函数¶
HTML
<!-- url_for - 生成URL -->
<a href="{{ url_for('main.index') }}">首页</a>
<a href="{{ url_for('user.profile', user_id=user.id) }}">个人资料</a>
<a href="{{ url_for('post.detail', post_id=post.id, slug=post.slug) }}">文章详情</a>
<!-- static - 静态文件URL -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
<!-- get_flashed_messages - 获取消息 -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- config - 访问配置 -->
<p>网站名称: {{ config.SITE_NAME }}</p>
2. 自定义全局函数¶
Python
# app/__init__.py
from flask import Flask
app = Flask(__name__)
@app.context_processor
def utility_processor():
"""注册全局函数"""
def format_price(price):
"""格式化价格"""
return f'¥{price:,.2f}'
def format_date(date):
"""格式化日期"""
return date.strftime('%Y年%m月%d日')
def is_admin(user):
"""检查是否为管理员"""
return user.is_authenticated and user.is_admin
return dict(
format_price=format_price,
format_date=format_date,
is_admin=is_admin
)
HTML
<!-- 使用自定义全局函数 -->
<p>价格: {{ format_price(product.price) }}</p>
<p>发布时间: {{ format_date(post.created_at) }}</p>
{% if is_admin(current_user) %}
<button class="btn btn-danger">删除</button>
{% endif %}
🎯 模板渲染¶
1. 渲染模板¶
Python
from flask import render_template, render_template_string
# 渲染模板文件
@app.route('/')
def index():
return render_template('index.html',
title='首页',
user=current_user,
posts=posts)
# 渲染模板字符串
@app.route('/inline')
def inline_template():
template = '''
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</body>
</html>
'''
return render_template_string(template,
title='内联模板',
content='这是内联渲染的内容')
2. 传递上下文¶
Python
@app.route('/user/<username>')
def user_profile(username):
user = User.query.filter_by(username=username).first_or_404()
context = {
'user': user,
'posts': user.posts.order_by(Post.created_at.desc()).limit(10).all(),
'is_following': current_user.is_following(user) if current_user.is_authenticated else False,
'title': f'{user.name} - 个人资料'
}
return render_template('user/profile.html', **context)
💡 最佳实践¶
1. 模板组织¶
Python
# 推荐的模板组织方式
template_organization = {
'使用继承': '通过base.html实现页面复用',
'模块化': '将重复的部分提取为组件',
'使用宏': '定义可复用的模板片段',
'分离关注点': '保持模板简洁,逻辑在Python中处理'
}
2. 性能优化¶
📝 练习题¶
基础题¶
- 什么是Jinja2模板引擎?
- 如何在模板中输出变量?
- 如何使用模板继承?
进阶题¶
- 创建自定义过滤器。
- 实现一个可复用的表单宏。
- 使用模板组件构建页面。
实践题¶
- 创建一个完整的博客模板系统。
- 实现用户资料页面。
- 构建一个响应式布局的网站。
📚 推荐阅读¶
🔗 下一章¶
表单处理 - 学习Flask的表单处理和验证。