第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.html

2.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不直接支持breakcontinue语句,但可以通过其他方式实现类似功能:

<!-- 使用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>&copy; 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&#39;&#39;) 连接列表 `{{ ['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&#39;&#39;, 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) }}{&#39;a&#39;: 1, &#39;b&#39;: 2}
lipsum(n=5, html=True) 生成Lorem Ipsum文本 {{ lipsum() }}
cycler(*items) 创建循环器 {{ cycler(&#39;odd&#39;, &#39;even&#39;) }}
joiner(sep=&#39;, &#39;) 创建连接符 {{ joiner(&#39;, &#39;) }}

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; &amp;lt;
&gt; &amp;gt;
&amp; &amp;amp;
&quot; &amp;quot;
&#39; &amp;#39;
<!-- 自动转义 -->
{{ '<script>alert("XSS")</script>' }}  <!-- 输出: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt; -->

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'] = False

10.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.html

11.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>&copy; 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)的各种功能和用法:

  1. 模板基础:模板文件位置、渲染模板、模板语法
  2. 模板变量:基本变量、复合变量、默认值
  3. 控制结构:条件语句、循环语句、循环变量
  4. 模板继承:基础模板、子模板、块、嵌套块
  5. :宏定义、使用、带HTML属性的宏
  6. 导入和包含includeimport
  7. 过滤器:内置过滤器、自定义过滤器
  8. 函数:内置函数、自定义函数
  9. 上下文:全局变量、上下文处理器
  10. 安全:自动转义、安全过滤器
  11. 最佳实践:模板设计原则、目录结构、性能优化

通过学习这些知识,你已经可以使用Jinja2模板引擎创建功能丰富、结构清晰、易于维护的Web页面。在实际开发中,合理利用模板的各种功能可以大大提高开发效率和代码质量。

« 上一篇 Flask路由与视图 下一篇 » Flask表单处理