跳转至

模板引擎

📖 章节简介

本章将介绍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
HTML
<!-- 使用自定义过滤器 -->
{{ text|reverse }}
{{ price|currency }}
{{ long_text|truncate_words(20) }}

🧩 模板继承

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>&copy; {{ 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>
HTML
<!-- 在其他模板中包含 -->
{% include 'components/navbar.html' %}

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. 性能优化

Python
# 启用模板缓存
app.config['TEMPLATES_AUTO_RELOAD'] = False  # 生产环境

# 预编译模板
# 在部署时预编译模板可以提高性能

📝 练习题

基础题

  1. 什么是Jinja2模板引擎?
  2. 如何在模板中输出变量?
  3. 如何使用模板继承?

进阶题

  1. 创建自定义过滤器。
  2. 实现一个可复用的表单宏。
  3. 使用模板组件构建页面。

实践题

  1. 创建一个完整的博客模板系统。
  2. 实现用户资料页面。
  3. 构建一个响应式布局的网站。

📚 推荐阅读

🔗 下一章

表单处理 - 学习Flask的表单处理和验证。