第137集 Flask模板
1. 模板系统概述
1.1 什么是模板
模板(Template)是一种包含动态内容占位符的文本文件,用于生成动态的HTML、XML或其他格式的响应。在Web开发中,模板主要用于将数据与表示层分离,使前端设计和后端逻辑可以独立开发和维护。
1.2 Jinja2模板引擎
Flask使用Jinja2作为默认的模板引擎。Jinja2是一个功能强大的Python模板引擎,由Flask的作者Armin Ronacher开发,具有以下特点:
- 强大的模板继承系统
- 支持复杂的控制结构(条件、循环等)
- 丰富的内置过滤器和函数
- 安全的模板执行环境(防止XSS攻击)
- 支持自定义过滤器和函数
- 支持宏(类似于函数的模板片段)
1.3 模板的作用
- 实现前后端分离
- 提高代码复用性
- 使页面设计更灵活
- 便于团队协作开发
- 提高代码可维护性
2. 模板基础
2.1 模板文件的位置
在Flask中,模板文件默认存放在应用根目录下的templates文件夹中:
myapp/
├── app.py
├── templates/
│ ├── index.html
│ ├── about.html
│ └── user/
│ └── profile.html2.2 渲染模板
使用render_template()函数渲染模板:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
# 渲染模板并传递数据
return render_template('index.html', name='张三', age=25)2.3 模板语法
Jinja2使用以下几种主要的语法结构:
| 语法 | 描述 | 示例 |
|---|---|---|
{{ ... }} |
变量输出 | {{ name }} |
{% ... %} |
控制结构 | {% if condition %} |
{# ... #} |
注释 | {# 这是注释 #} |
3. 模板变量
3.1 基本变量
模板中可以直接输出Python变量:
# 传递基本数据类型
return render_template('index.html',
name='张三',
age=25,
is_student=True,
score=98.5)在模板中使用:
<h1>欢迎,{{ name }}!</h1>
<p>年龄:{{ age }}</p>
<p>是否是学生:{{ is_student }}</p>
<p>成绩:{{ score }}</p>3.2 复合变量
可以传递列表、字典、对象等复合数据类型:
# 传递列表
fruits = ['苹果', '香蕉', '橙子']
# 传递字典
user = {
'name': '张三',
'age': 25,
'email': 'zhangsan@example.com'
}
# 传递对象
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user_obj = User('李四', 30)
return render_template('index.html',
fruits=fruits,
user=user,
user_obj=user_obj)在模板中使用:
<!-- 列表 -->
<h2>水果列表</h2>
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
<!-- 字典 -->
<h2>用户信息(字典)</h2>
<p>姓名:{{ user.name }} 或 {{ user['name'] }}</p>
<p>年龄:{{ user.age }} 或 {{ user['age'] }}</p>
<!-- 对象 -->
<h2>用户信息(对象)</h2>
<p>姓名:{{ user_obj.name }}</p>
<p>年龄:{{ user_obj.age }}</p>3.3 变量的默认值
可以使用|default过滤器为变量提供默认值:
<p>昵称:{{ nickname|default('匿名用户') }}</p>4. 模板控制结构
4.1 条件语句
使用{% if %}、{% elif %}、{% else %}和{% endif %}实现条件判断:
<!-- 基本条件 -->
{% if age >= 18 %}
<p>成年人</p>
{% else %}
<p>未成年人</p>
{% endif %}
<!-- 多条件 -->
{% if score >= 90 %}
<p>优秀</p>
{% elif score >= 80 %}
<p>良好</p>
{% elif score >= 60 %}
<p>及格</p>
{% else %}
<p>不及格</p>
{% endif %}
<!-- 逻辑运算符 -->
{% if is_student and age < 20 %}
<p>年轻学生</p>
{% endif %}
{% if not is_login %}
<p>请登录</p>
{% endif %}4.2 循环语句
使用{% for %}和{% endfor %}实现循环:
4.2.1 基本循环
<!-- 遍历列表 -->
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
<!-- 遍历字典 -->
<ul>
{% for key, value in user.items() %}
<li>{{ key }}: {{ value }}</li>
{% endfor %}
</ul>4.2.2 循环变量
在循环中可以使用特殊的循环变量:
| 变量 | 描述 |
|---|---|
loop.index |
当前迭代的索引(从1开始) |
loop.index0 |
当前迭代的索引(从0开始) |
loop.first |
是否是第一次迭代 |
loop.last |
是否是最后一次迭代 |
loop.length |
序列的长度 |
loop.revindex |
反向索引(从length开始) |
loop.revindex0 |
反向索引(从length-1开始) |
loop.cycle |
在给定的序列中循环取值 |
<!-- 使用循环变量 -->
<ul>
{% for item in items %}
<li>第{{ loop.index }}项: {{ item }}</li>
{% endfor %}
</ul>
<!-- 奇偶行不同样式 -->
<table>
{% for user in users %}
<tr class="{{ loop.cycle('odd', 'even') }}">
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
</tr>
{% endfor %}
</table>4.2.3 空循环处理
使用{% else %}处理空序列:
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% else %}
<li>没有水果</li>
{% endfor %}
</ul>4.2.4 循环控制
Jinja2不直接支持break和continue语句,但可以通过其他方式实现类似功能:
<!-- 使用if跳过某些项(类似continue) -->
<ul>
{% for item in items %}
{% if item != '跳过' %}
<li>{{ item }}</li>
{% endif %}
{% endfor %}
</ul>4.3 宏(Macros)
宏类似于函数,可以定义可重用的模板片段:
4.3.1 定义宏
{% macro input(name, value='', type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}4.3.2 使用宏
<!-- 使用宏 -->
<form>
<p>用户名:{{ input('username') }}</p>
<p>密码:{{ input('password', type='password') }}</p>
<p>邮箱:{{ input('email', value='user@example.com') }}</p>
<p>{{ input('submit', value='提交', type='submit') }}</p>
</form>4.3.3 带默认值和HTML属性的宏
{% macro input(name, value='', type='text', **kwargs) %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}"
{% for key, value in kwargs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
>
{% endmacro %}
<!-- 使用带HTML属性的宏 -->
{{ input('username', placeholder='请输入用户名', class='form-input', required=true) }}4.4 导入和包含
4.4.1 include语句
使用{% include %}包含其他模板文件:
<!-- header.html -->
<header>
<h1>网站标题</h1>
<nav>导航菜单</nav>
</header>
<!-- index.html -->
{% include 'header.html' %}
<main>页面内容</main>
{% include 'footer.html' %}4.4.2 import语句
使用{% import %}导入其他模板中的宏或变量:
<!-- macros.html -->
{% macro input(name, value='', type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% macro button(text, type='button') %}
<button type="{{ type }}">{{ text }}</button>
{% endmacro %}
<!-- index.html -->
{% import 'macros.html' as forms %}
<form>
<p>{{ forms.input('username') }}</p>
<p>{{ forms.button('提交', type='submit') }}</p>
</form>
<!-- 只导入特定宏 -->
{% from 'macros.html' import input, button %}
<p>{{ input('email') }}</p>
<p>{{ button('取消') }}</p>5. 模板继承
模板继承是Jinja2的核心功能之一,允许创建一个基础模板(base template),然后在子模板中继承并扩展它。
5.1 基础模板(Base Template)
基础模板定义了页面的基本结构,使用{% block %}标记可以被子模板重写的部分:
<!-- base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{% block title %}默认标题{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block styles %}{% endblock %}
</head>
<body>
<!-- 头部 -->
<header>
<h1>网站标题</h1>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
<a href="/contact">联系</a>
</nav>
</header>
<!-- 主要内容区域 -->
<main>
{% block content %}
<p>默认内容</p>
{% endblock %}
</main>
<!-- 页脚 -->
<footer>
<p>© 2023 网站名称</p>
</footer>
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>5.2 子模板(Child Template)
子模板使用{% extends %}继承基础模板,并使用{% block %}重写基础模板中的块:
<!-- index.html -->
{% extends 'base.html' %}
{% block title %}
首页 - 网站名称
{% endblock %}
{% block content %}
<h2>欢迎来到首页</h2>
<p>这是首页的内容</p>
{% endblock %}
{% block scripts %}
<script>
// 首页特有的JavaScript代码
console.log('首页加载完成');
</script>
{% endblock %}
<!-- about.html -->
{% extends 'base.html' %}
{% block title %}
关于我们 - 网站名称
{% endblock %}
{% block content %}
<h2>关于我们</h2>
<p>这是关于我们的内容</p>
{% endblock %}5.3 块的覆盖与扩展
使用{{ super() }}可以在子模板中调用基础模板中块的内容:
<!-- base.html -->
{% block styles %}
<link rel="stylesheet" href="base.css">
{% endblock %}
<!-- index.html -->
{% block styles %}
{{ super() }} <!-- 包含基础模板的样式 -->
<link rel="stylesheet" href="index.css"> <!-- 添加首页特有的样式 -->
{% endblock %}5.4 嵌套块
块可以嵌套使用:
<!-- base.html -->
{% block content %}
<div class="container">
{% block main_content %}
默认内容
{% endblock %}
</div>
{% endblock %}
<!-- index.html -->
{% extends 'base.html' %}
{% block content %}
{{ super() }}
<div class="sidebar">侧边栏内容</div>
{% endblock %}
{% block main_content %}
<h2>首页主要内容</h2>
<p>这是首页的主要内容</p>
{% endblock %}6. 模板过滤器
过滤器用于修改变量的值,使用管道符号(|)连接。
6.1 内置过滤器
Jinja2提供了丰富的内置过滤器:
6.1.1 字符串过滤器
| 过滤器 | 描述 | 示例 |
|---|---|---|
capitalize |
首字母大写,其余小写 | `{{ 'hello' |
upper |
全部大写 | `{{ 'hello' |
lower |
全部小写 | `{{ 'HELLO' |
title |
每个单词首字母大写 | `{{ 'hello world' |
trim |
去除首尾空白 | `{{ ' hello ' |
striptags |
去除HTML标签 | `{{ ' hello ' |
truncate(length=255, killwords=False) |
截断字符串 | `{{ 'Hello World' |
replace(old, new) |
替换字符串 | `{{ 'Hello' |
safe |
标记为安全内容(禁用自动转义) | `{{ 'text' |
6.1.2 数字过滤器
| 过滤器 | 描述 | 示例 |
|---|---|---|
abs |
绝对值 | `{{ -10 |
round(precision=0) |
四舍五入 | `{{ 3.14159 |
int |
转换为整数 | `{{ '10' |
float |
转换为浮点数 | `{{ '10.5' |
6.1.3 列表过滤器
| 过滤器 | 描述 | 示例 |
|---|---|---|
first |
第一个元素 | `{{ [1, 2, 3] |
last |
最后一个元素 | `{{ [1, 2, 3] |
length |
列表长度 | `{{ [1, 2, 3] |
sum |
求和 | `{{ [1, 2, 3] |
sort |
排序 | `{{ [3, 1, 2] |
reverse |
反转 | `{{ [1, 2, 3] |
join(d=u'') |
连接列表 | `{{ ['a', 'b', 'c'] |
unique |
去重 | `{{ [1, 2, 2, 3] |
6.1.4 字典过滤器
| 过滤器 | 描述 | 示例 |
|---|---|---|
keys |
获取所有键 | `{{ {'a': 1, 'b': 2} |
values |
获取所有值 | `{{ {'a': 1, 'b': 2} |
items |
获取所有键值对 | `{{ {'a': 1, 'b': 2} |
6.1.5 其他过滤器
| 过滤器 | 描述 | 示例 |
|---|---|---|
default(value, default_value=u'', boolean=False) |
默认值 | `{{ none_value |
format(*args, **kwargs) |
字符串格式化 | `{{ '%s is %d' |
dictsort |
字典排序 | `{{ {'b': 2, 'a': 1} |
6.2 自定义过滤器
可以在Flask应用中注册自定义过滤器:
6.2.1 使用装饰器注册
from flask import Flask
app = Flask(__name__)
# 自定义过滤器:将字符串反转
@app.template_filter('reverse')
def reverse_filter(s):
return s[::-1]
# 自定义过滤器:格式化日期
@app.template_filter('format_date')
def format_date_filter(date, format='%Y-%m-%d'):
return date.strftime(format)在模板中使用:
{{ 'hello'|reverse }} <!-- 输出: olleh -->
{{ today|format_date }} <!-- 输出: 2023-12-25 -->
{{ today|format_date('%Y年%m月%d日') }} <!-- 输出: 2023年12月25日 -->6.2.2 手动注册
# 定义过滤器函数
def capitalize_filter(s):
if not s: return ''
return s[0].upper() + s[1:]
# 手动注册过滤器
app.add_template_filter(capitalize_filter, 'capitalize')7. 模板函数
7.1 内置函数
Jinja2提供了一些内置函数:
| 函数 | 描述 | 示例 |
|---|---|---|
range([start,] stop[, step]) |
生成范围 | {% for i in range(5) %} → 0, 1, 2, 3, 4 |
dict([items]) |
创建字典 | {{ dict(a=1, b=2) }} → {'a': 1, 'b': 2} |
lipsum(n=5, html=True) |
生成Lorem Ipsum文本 | {{ lipsum() }} |
cycler(*items) |
创建循环器 | {{ cycler('odd', 'even') }} |
joiner(sep=', ') |
创建连接符 | {{ joiner(', ') }} |
7.2 自定义函数
可以在Flask应用中注册自定义函数:
# 自定义函数:计算平方根
import math
def square_root(n):
return math.sqrt(n)
# 注册自定义函数
app.jinja_env.globals['square_root'] = square_root
# 或者使用装饰器
@app.template_global()
def cube(n):
return n ** 3在模板中使用:
{{ square_root(16) }} <!-- 输出: 4.0 -->
{{ cube(3) }} <!-- 输出: 27 -->8. 模板上下文
8.1 全局上下文
Flask自动在模板上下文中提供了一些全局变量:
| 变量 | 描述 |
|---|---|
config |
当前应用的配置对象 |
request |
当前请求对象 |
session |
当前会话对象 |
g |
请求上下文的全局对象 |
url_for() |
用于构建URL的函数 |
get_flashed_messages() |
获取闪现消息 |
<!-- 使用全局变量 -->
<p>应用名称:{{ config['SECRET_KEY'] }}</p>
<p>请求URL:{{ request.url }}</p>
<p>用户名:{{ session['username'] }}</p>
<p><a href="{{ url_for('index') }}">首页</a></p>
<!-- 闪现消息 -->
{% for message in get_flashed_messages() %}
<div class="flash-message">{{ message }}</div>
{% endfor %}8.2 上下文处理器
可以使用上下文处理器(Context Processor)向所有模板添加自定义变量:
# 上下文处理器:向所有模板添加当前时间
@app.context_processor
def inject_now():
return {'now': datetime.datetime.now()}
# 上下文处理器:添加多个变量
@app.context_processor
def inject_user():
user = {'name': '张三', 'age': 25}
return {'user': user, 'title': '我的网站'}在模板中使用:
<p>当前时间:{{ now.strftime('%Y-%m-%d %H:%M:%S') }}</p>
<p>用户:{{ user.name }}</p>
<p>标题:{{ title }}</p>9. 模板安全
9.1 自动转义
为了防止跨站脚本攻击(XSS),Jinja2默认会自动转义HTML特殊字符:
| 原字符 | 转义后 |
|---|---|
< |
&lt; |
> |
&gt; |
& |
&amp; |
" |
&quot; |
' |
&#39; |
<!-- 自动转义 -->
{{ '<script>alert("XSS")</script>' }} <!-- 输出: <script>alert("XSS")</script> -->9.2 禁用自动转义
在确保内容安全的情况下,可以使用safe过滤器禁用自动转义:
<!-- 禁用自动转义 -->
{{ '<strong>粗体文本</strong>'|safe }} <!-- 输出: <strong>粗体文本</strong> -->9.3 选择性转义
使用{% autoescape %}标签可以控制自动转义的开启和关闭:
{% autoescape true %}
<!-- 自动转义开启 -->
{{ '<script>alert("XSS")</script>' }}
{% endautoescape %}
{% autoescape false %}
<!-- 自动转义关闭 -->
{{ '<strong>粗体文本</strong>' }}
{% endautoescape %}10. 模板优化
10.1 模板缓存
在生产环境中,Flask会自动缓存编译后的模板以提高性能。可以通过配置调整缓存行为:
# 禁用模板缓存(开发环境默认)
app.config['TEMPLATES_AUTO_RELOAD'] = True
# 启用模板缓存(生产环境默认)
app.config['TEMPLATES_AUTO_RELOAD'] = False10.2 性能优化建议
- 减少模板中的复杂计算
- 合理使用模板继承,避免过度嵌套
- 避免在模板中进行数据库查询
- 使用缓存机制存储频繁使用的模板片段
- 减少模板中的条件判断和循环复杂度
11. 模板最佳实践
11.1 模板设计原则
- 保持模板简洁,避免复杂逻辑
- 只在模板中处理表示层逻辑
- 充分利用模板继承和宏提高复用性
- 使用语义化的块名称
- 保持一致的命名约定
11.2 目录结构
对于大型应用,建议按功能模块组织模板:
templates/
├── base.html # 基础模板
├── index.html # 首页模板
├── auth/ # 认证模块模板
│ ├── login.html
│ └── register.html
├── user/ # 用户模块模板
│ ├── profile.html
│ └── settings.html
└── post/ # 文章模块模板
├── list.html
└── detail.html11.3 与CSS/JavaScript集成
在模板中正确引用静态资源:
<!-- 引用CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- 引用JavaScript -->
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
<!-- 内联JavaScript使用模板变量 -->
<script>
var username = '{{ user.name|tojson|safe }}';
console.log('当前用户:' + username);
</script>12. 综合示例
下面是一个综合示例,展示了模板的各种高级用法:
from flask import Flask, render_template, url_for
from datetime import datetime
app = Flask(__name__)
# 自定义过滤器
@app.template_filter('format_date')
def format_date_filter(date, format='%Y-%m-%d'):
return date.strftime(format)
# 上下文处理器
@app.context_processor
def inject_now():
return {'now': datetime.now()}
@app.route('/')
def index():
# 准备模板数据
user = {
'name': '张三',
'age': 25,
'email': 'zhangsan@example.com',
'is_vip': True,
'score': 95.5
}
fruits = ['苹果', '香蕉', '橙子', '草莓', '葡萄']
articles = [
{'title': 'Flask入门教程', 'content': '这是一篇关于Flask的入门教程', 'date': datetime(2023, 12, 1)},
{'title': 'Jinja2模板使用指南', 'content': '详细介绍Jinja2模板的使用方法', 'date': datetime(2023, 12, 15)},
{'title': 'Python Web开发最佳实践', 'content': '分享Python Web开发的最佳实践', 'date': datetime(2023, 12, 25)}
]
return render_template('index.html', user=user, fruits=fruits, articles=articles)
if __name__ == '__main__':
app.run(debug=True)<!-- base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{% block title %}我的网站{% endblock %}</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
header { background-color: #f0f0f0; padding: 10px; margin-bottom: 20px; }
footer { background-color: #f0f0f0; padding: 10px; margin-top: 20px; text-align: center; }
.article { border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; }
.vip { color: red; font-weight: bold; }
</style>
</head>
<body>
<header>
<h1>{% block header %}我的网站{% endblock %}</h1>
<nav>
<a href="{{ url_for('index') }}">首页</a> |
<a href="#">关于</a> |
<a href="#">联系</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2023 我的网站 | 当前时间:{{ now|format_date('%Y年%m月%d日 %H:%M:%S') }}</p>
</footer>
</body>
</html>
<!-- index.html -->
{% extends 'base.html' %}
{% block title %}
首页 - 我的网站
{% endblock %}
{% block content %}
<h2>欢迎访问我的网站</h2>
<!-- 用户信息 -->
<div class="user-info">
<h3>用户信息</h3>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<p>邮箱:{{ user.email }}</p>
<p>会员状态:{% if user.is_vip %}<span class="vip">VIP会员</span>{% else %}普通会员{% endif %}</p>
<p>积分:{{ user.score }} 分</p>
</div>
<!-- 水果列表 -->
<div class="fruits">
<h3>水果列表</h3>
<ul>
{% for fruit in fruits %}
<li>{{ loop.index }}. {{ fruit }}</li>
{% endfor %}
</ul>
<p>共有 {{ fruits|length }} 种水果</p>
</div>
<!-- 文章列表 -->
<div class="articles">
<h3>最新文章</h3>
{% for article in articles %}
<div class="article">
<h4>{{ article.title }}</h4>
<p>{{ article.content|truncate(100) }}</p>
<p class="article-date">发布日期:{{ article.date|format_date('%Y-%m-%d') }}</p>
</div>
{% endfor %}
</div>
<!-- 欢迎信息 -->
{% if user.is_vip %}
<div class="vip-welcome">
<h3>欢迎您,尊贵的VIP会员!</h3>
<p>您可以享受以下特权:</p>
<ul>
<li>无广告阅读</li>
<li>优先阅读最新文章</li>
<li>专属客服支持</li>
</ul>
</div>
{% else %}
<div class="upgrade-vip">
<h3>升级为VIP会员</h3>
<p>点击<a href="#">这里</a>升级为VIP会员,享受更多特权!</p>
</div>
{% endif %}
{% endblock %}13. 总结
本集主要介绍了Flask模板系统(Jinja2)的各种功能和用法:
- 模板基础:模板文件位置、渲染模板、模板语法
- 模板变量:基本变量、复合变量、默认值
- 控制结构:条件语句、循环语句、循环变量
- 模板继承:基础模板、子模板、块、嵌套块
- 宏:宏定义、使用、带HTML属性的宏
- 导入和包含:
include、import - 过滤器:内置过滤器、自定义过滤器
- 函数:内置函数、自定义函数
- 上下文:全局变量、上下文处理器
- 安全:自动转义、安全过滤器
- 最佳实践:模板设计原则、目录结构、性能优化
通过学习这些知识,你已经可以使用Jinja2模板引擎创建功能丰富、结构清晰、易于维护的Web页面。在实际开发中,合理利用模板的各种功能可以大大提高开发效率和代码质量。