Flask模板继承

模板继承是Jinja2最强大的功能之一,允许你创建一个基础模板框架,然后在子模板中扩展和覆盖特定部分。

1. 什么是模板继承

优点
  • 代码重用:避免重复的HTML结构
  • 一致性:保持网站外观统一
  • 易于维护:修改一处,全局生效
  • 模块化:分离结构和内容
  • 灵活性:可覆盖或扩展特定部分
继承关系图
base.html
home.html
about.html
contact.html

多个页面共享相同的布局,但内容不同

2. 基础模板创建

基础模板定义了网站的基本结构,使用 {% block %} 标签定义可被子模板覆盖的区域。

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>

    <!-- Bootstrap 5 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">

    {% block styles %}{% endblock %}

    <!-- 全局CSS -->
    <style>
        :root {
            --primary-color: #007bff;
            --secondary-color: #6c757d;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            padding-top: 56px; /* 导航栏高度 */
        }

        .content-wrapper {
            min-height: calc(100vh - 200px);
        }
    </style>
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
        <div class="container">
            <a class="navbar-brand" href="{{ url_for('index') }}">
                <i class="fas fa-rocket me-2"></i>MySite
            </a>

            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarNav">
                {% block navbar %}
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('index') }}">首页</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('about') }}">关于</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('contact') }}">联系</a>
                    </li>
                </ul>

                <ul class="navbar-nav">
                    {% if current_user.is_authenticated %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('profile') }}">
                                <i class="fas fa-user me-1"></i>{{ current_user.username }}
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('logout') }}">退出</a>
                        </li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('login') }}">登录</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{{ url_for('register') }}">注册</a>
                        </li>
                    {% endif %}
                </ul>
                {% endblock %}
            </div>
        </div>
    </nav>

    <!-- 内容区域 -->
    <div class="container content-wrapper">
        <!-- 面包屑导航 -->
        {% block breadcrumb %}
        <nav aria-label="breadcrumb" class="mt-3">
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="{{ url_for('index') }}">首页</a></li>
                {% block breadcrumb_items %}{% endblock %}
            </ol>
        </nav>
        {% endblock %}

        <!-- 主内容块 -->
        <main>
            {% block content %}
            <div class="alert alert-info">
                <i class="fas fa-info-circle me-2"></i>
                这是基础模板的默认内容。请在子模板中覆盖此块。
            </div>
            {% endblock %}
        </main>
    </div>

    <!-- 页脚 -->
    <footer class="bg-dark text-white py-4 mt-5">
        <div class="container">
            {% block footer %}
            <div class="row">
                <div class="col-md-4">
                    <h5>MySite</h5>
                    <p>一个使用Flask构建的示例网站</p>
                </div>
                <div class="col-md-4">
                    <h5>快速链接</h5>
                    <ul class="list-unstyled">
                        <li><a href="{{ url_for('index') }}" class="text-white-50">首页</a></li>
                        <li><a href="{{ url_for('about') }}" class="text-white-50">关于我们</a></li>
                        <li><a href="{{ url_for('contact') }}" class="text-white-50">联系我们</a></li>
                    </ul>
                </div>
                <div class="col-md-4">
                    <h5>联系我们</h5>
                    <p><i class="fas fa-envelope me-2"></i> contact@example.com</p>
                    <p><i class="fas fa-phone me-2"></i> +86 123-4567-8901</p>
                </div>
            </div>
            <div class="text-center mt-3 pt-3 border-top border-secondary">
                <p class="mb-0">© 2023 MySite. 保留所有权利.</p>
            </div>
            {% endblock %}
        </div>
    </footer>

    <!-- 脚本 -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>

    {% block scripts %}{% endblock %}

    <!-- 全局脚本 -->
    <script>
        // 全局JavaScript代码
        document.addEventListener('DOMContentLoaded', function() {
            // 工具提示初始化
            var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
            var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
                return new bootstrap.Tooltip(tooltipTriggerEl);
            });
        });
    </script>
</body>
</html>
基础模板要点
  • 定义网站的整体HTML结构
  • 使用 {% block block_name %}{% endblock %} 定义可覆盖区域
  • 可以提供块的默认内容(可选)
  • 通常包含页头、导航栏、页脚等公共元素
  • 可以包含全局的CSS和JavaScript

3. 子模板扩展

子模板使用 {% extends "base.html" %} 来继承基础模板,然后覆盖或扩展特定的块。

templates/index.html - 首页
{% extends "base.html" %}

{# 覆盖标题块 #}
{% block title %}欢迎来到我的网站 - 首页{% endblock %}

{# 添加额外的CSS #}
{% block styles %}
<style>
    .hero-section {
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        padding: 100px 0;
        border-radius: 10px;
        margin-bottom: 40px;
    }

    .feature-card {
        transition: transform 0.3s;
    }

    .feature-card:hover {
        transform: translateY(-5px);
    }
</style>
{% endblock %}

{# 覆盖导航栏块 #}
{% block navbar %}
{{ super() }}  {# 包含父模板的内容 #}
<!-- 可以添加额外的导航项 -->
<ul class="navbar-nav ms-auto">
    <li class="nav-item">
        <a class="nav-link" href="{{ url_for('blog') }}">
            <i class="fas fa-blog me-1"></i>博客
        </a>
    </li>
</ul>
{% endblock %}

{# 覆盖面包屑项目 #}
{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">首页</li>
{% endblock %}

{# 覆盖主内容块 #}
{% block content %}
<!-- 英雄区域 -->
<div class="hero-section text-center">
    <h1 class="display-4 fw-bold">欢迎来到我们的网站</h1>
    <p class="lead">我们提供最优质的服务和最先进的解决方案</p>
    <a href="{{ url_for('about') }}" class="btn btn-light btn-lg mt-3">
        了解更多
    </a>
</div>

<!-- 特性展示 -->
<div class="row">
    <div class="col-md-4 mb-4">
        <div class="card feature-card h-100">
            <div class="card-body text-center">
                <i class="fas fa-bolt fa-3x text-primary mb-3"></i>
                <h5 class="card-title">快速高效</h5>
                <p class="card-text">我们的服务响应迅速,确保最佳用户体验。</p>
            </div>
        </div>
    </div>
    <div class="col-md-4 mb-4">
        <div class="card feature-card h-100">
            <div class="card-body text-center">
                <i class="fas fa-shield-alt fa-3x text-success mb-3"></i>
                <h5 class="card-title">安全可靠</h5>
                <p class="card-text">采用最先进的安全技术保护您的数据。</p>
            </div>
        </div>
    </div>
    <div class="col-md-4 mb-4">
        <div class="card feature-card h-100">
            <div class="card-body text-center">
                <i class="fas fa-headset fa-3x text-warning mb-3"></i>
                <h5 class="card-title">优质支持</h5>
                <p class="card-text">24/7客户支持,随时为您解决问题。</p>
            </div>
        </div>
    </div>
</div>

<!-- 动态内容 -->
{% if recent_posts %}
<div class="mt-5">
    <h2 class="mb-4">最新文章</h2>
    <div class="row">
        {% for post in recent_posts %}
        <div class="col-lg-4 col-md-6 mb-4">
            <div class="card h-100">
                <div class="card-body">
                    <h5 class="card-title">{{ post.title }}</h5>
                    <p class="card-text">{{ post.excerpt|truncate(100) }}</p>
                    <small class="text-muted">
                        <i class="fas fa-calendar me-1"></i>
                        {{ post.created_at|date('Y-m-d') }}
                    </small>
                </div>
                <div class="card-footer bg-transparent border-top-0">
                    <a href="{{ url_for('post_detail', slug=post.slug) }}"
                       class="btn btn-outline-primary btn-sm">
                        阅读更多
                    </a>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>
</div>
{% endif %}
{% endblock %}

{# 添加额外的JavaScript #}
{% block scripts %}
<script>
    // 首页特定的JavaScript
    document.addEventListener('DOMContentLoaded', function() {
        console.log('首页加载完成');

        // 为特性卡片添加点击事件
        document.querySelectorAll('.feature-card').forEach(card => {
            card.addEventListener('click', function() {
                this.classList.toggle('shadow-lg');
            });
        });
    });
</script>
{% endblock %}
templates/about.html - 关于页面
{% extends "base.html" %}

{% block title %}关于我们 - 我的网站{% endblock %}

{% block styles %}
<style>
    .team-member {
        text-align: center;
        padding: 20px;
    }

    .team-member img {
        width: 150px;
        height: 150px;
        border-radius: 50%;
        object-fit: cover;
        margin-bottom: 15px;
    }

    .timeline {
        position: relative;
        padding-left: 30px;
    }

    .timeline:before {
        content: '';
        position: absolute;
        left: 15px;
        top: 0;
        bottom: 0;
        width: 2px;
        background: #007bff;
    }
</style>
{% endblock %}

{% block breadcrumb_items %}
<li class="breadcrumb-item active" aria-current="page">关于我们</li>
{% endblock %}

{% block content %}
<h1 class="mb-4">关于我们</h1>

<div class="row">
    <div class="col-lg-6">
        <h2>我们的故事</h2>
        <p>我们成立于2010年,一直致力于为客户提供最优质的技术解决方案。</p>
        <p>我们的使命是通过创新技术让世界变得更美好。</p>
    </div>
    <div class="col-lg-6">
        <h2>我们的价值观</h2>
        <ul>
            <li><strong>创新</strong>:不断探索新技术</li>
            <li><strong>诚信</strong>:诚实对待每一位客户</li>
            <li><strong>卓越</strong>:追求最高标准的质量</li>
            <li><strong>合作</strong>:团队合作共创价值</li>
        </ul>
    </div>
</div>

<!-- 时间线 -->
<div class="mt-5">
    <h2>发展历程</h2>
    <div class="timeline mt-4">
        {% for milestone in milestones %}
        <div class="mb-4 position-relative">
            <div class="position-absolute" style="left: -30px; top: 0;">
                <div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center"
                     style="width: 30px; height: 30px;">
                    {{ loop.index }}
                </div>
            </div>
            <h5>{{ milestone.year }} - {{ milestone.title }}</h5>
            <p>{{ milestone.description }}</p>
        </div>
        {% endfor %}
    </div>
</div>

<!-- 团队成员 -->
<div class="mt-5">
    <h2 class="mb-4">我们的团队</h2>
    <div class="row">
        {% for member in team_members %}
        <div class="col-md-4 mb-4">
            <div class="team-member">
                <img src="{{ member.avatar }}" alt="{{ member.name }}">
                <h5>{{ member.name }}</h5>
                <p class="text-muted">{{ member.position }}</p>
                <p>{{ member.bio }}</p>
                <div>
                    {% if member.linkedin %}
                    <a href="{{ member.linkedin }}" class="text-decoration-none me-2">
                        <i class="fab fa-linkedin fa-lg"></i>
                    </a>
                    {% endif %}
                    {% if member.github %}
                    <a href="{{ member.github }}" class="text-decoration-none me-2">
                        <i class="fab fa-github fa-lg"></i>
                    </a>
                    {% endif %}
                </div>
            </div>
        </div>
        {% endfor %}
    </div>
</div>
{% endblock %}

{% block footer %}
{# 完全覆盖页脚,不显示父模板内容 #}
<footer class="bg-dark text-white py-4 mt-5">
    <div class="container text-center">
        <p>© 2023 关于页面特别页脚</p>
        <p><small>这个页面的页脚与基础模板不同</small></p>
    </div>
</footer>
{% endblock %}

4. 块(Blocks)详解

4.1 块的类型

有默认内容的块
{% block title %}
默认标题
{% endblock %}

子模板可以不覆盖,使用默认内容

空块
{% block styles %}
{% endblock %}

子模板可以填充内容

嵌套块
{% block sidebar %}
  {% block sidebar_menu %}
  {% endblock %}
{% endblock %}

块中可以包含其他块

4.2 块的作用域

块作用域示例
{# base.html #}
<html>
<body>
  {% block content %}
    {% block header %}
      <h1>默认标题</h1>
    {% endblock %}

    {% block main %}
      <p>默认内容</p>
    {% endblock %}
  {% endblock %}
</body>
</html>
{# child.html #}
{% extends "base.html" %}

{# 直接覆盖content块 #}
{% block content %}
  <div class="container">
    {{ super() }}  {# 包含父模板的header和main块 #}
  </div>
{% endblock %}

{# 或者单独覆盖某个块 #}
{% block header %}
  <h1 class="special">特殊标题</h1>
{% endblock %}

5. 超级块(Super Blocks)

{{ super() }} 用于在子模板中引用父模板中同名块的内容。

使用方法
{# 父模板 #}
{% block head %}
<meta name="description" content="默认描述">
<link rel="stylesheet" href="/css/base.css">
{% endblock %}

{# 子模板 #}
{% block head %}
{{ super() }}  {# 包含父模板的内容 #}

{# 添加额外内容 #}
<meta name="keywords" content="额外关键字">
<link rel="stylesheet" href="/css/custom.css">
{% endblock %}
生成结果
<meta name="description" content="默认描述">
<link rel="stylesheet" href="/css/base.css">

<meta name="keywords" content="额外关键字">
<link rel="stylesheet" href="/css/custom.css">

父模板内容 + 子模板内容

5.1 超级块的实际应用

实际应用:累积样式和脚本

{# base.html #}
{% block scripts %}
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
{% endblock %}
{# child.html #}
{% block scripts %}
{{ super() }}
<script src="/js/page-specific.js"></script>
{% endblock %}

{# grandchild.html #}
{% block scripts %}
{{ super() }}
<script src="/js/even-more-specific.js"></script>
{% endblock %}

最终结果:所有层级的脚本都会被包含,顺序是:父 → 子 → 孙

6. 嵌套继承

可以创建多层继承结构,形成更细粒度的模板层次。

多层继承关系
base.html
section_base.html
blog_post.html
product_page.html
base.html
<!DOCTYPE html>
<html>
{% block head %}{% endblock %}
<body>
  {% block body %}
    {% block header %}{% endblock %}
    {% block main %}{% endblock %}
    {% block footer %}{% endblock %}
  {% endblock %}
</body>
</html>
section_base.html
{% extends "base.html" %}

{% block head %}
{{ super() }}
<!-- 特定区域的样式 -->
<link rel="stylesheet" href="/css/section.css">
{% endblock %}

{% block header %}
<nav>区域导航</nav>
{% endblock %}

{% block main %}
<div class="section-content">
  {% block content %}{% endblock %}
</div>
{% endblock %}
blog_post.html
{% extends "section_base.html" %}

{% block head %}
{{ super() }}
<!-- 博客文章特定样式 -->
<link rel="stylesheet" href="/css/blog.css">
{% endblock %}

{% block content %}
<article>
  <h1>{{ post.title }}</h1>
  <div>{{ post.content }}</div>
</article>
{% endblock %}

7. 包含 vs 继承

特性 {% extends %} {% include %}
目的 创建模板层次结构 引入可重用组件
用法 必须是模板的第一个标签 可以在模板的任何位置使用
数量 只能继承一个模板 可以包含多个模板
内容 覆盖父模板的块 直接插入整个模板内容
适合场景 页面布局框架 导航栏、页脚、侧边栏等组件
变量作用域 继承所有变量 可以传递特定变量

7.1 结合使用示例

继承与包含结合使用

{# base.html #}
<!DOCTYPE html>
<html>
<body>
  {% include 'navbar.html' %}

  
{% block content %}{% endblock %}
{% include 'footer.html' %} </body> </html> {# home.html #} {% extends "base.html" %} {% block content %} <h1>首页</h1> {% include 'featured_products.html' %} {% include 'latest_news.html' %} {% endblock %}

8. 最佳实践

推荐做法
  • 单一基础模板:为整个网站创建一个基础模板
  • 明确命名块:使用有意义的块名称
  • 提供默认内容:为重要块提供合理的默认内容
  • 使用超级块:避免重复父模板内容
  • 分离结构:将样式和脚本放在单独的块中
  • 保持扁平:避免过深的继承层次(≤3层)
避免做法
  • 不要过度继承:避免创建太多中间层
  • 避免块嵌套过深:保持块结构简单明了
  • 不要忘记超级块:除非你确定要完全覆盖
  • 避免硬编码:使用变量和配置文件
  • 不要混合逻辑:模板中不应有复杂业务逻辑
  • 避免重复块名:确保块名称唯一

9. 完整示例:博客系统

博客系统模板结构
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>
    <link href="/css/bootstrap.min.css" rel="stylesheet">
    {% block extra_css %}{% endblock %}
</head>
<body>
    <div id="app">
        {% include 'partials/header.html' %}

        <div class="container mt-4">
            {% block content_wrapper %}
                {% block content %}{% endblock %}
            {% endblock %}
        </div>

        {% include 'partials/footer.html' %}
    </div>

    <script src="/js/bootstrap.bundle.min.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>
blog_base.html - 博客专用基础模板
{% extends "base.html" %}

{% block extra_css %}
{{ super() }}
<link href="/css/blog.css" rel="stylesheet">
{% endblock %}

{% block content_wrapper %}
<div class="row">
    <!-- 主要内容 -->
    <div class="col-md-8">
        {% block blog_content %}{% endblock %}
    </div>

    <!-- 侧边栏 -->
    <div class="col-md-4">
        {% block sidebar %}
            {% include 'blog/partials/sidebar.html' %}
        {% endblock %}
    </div>
</div>
{% endblock %}

{% block extra_js %}
{{ super() }}
<script src="/js/blog.js"></script>
{% endblock %}
blog/post_detail.html - 文章详情页
{% extends "blog_base.html" %}

{% block title %}{{ post.title }} - 我的博客{% endblock %}

{% block blog_content %}
<article class="blog-post">
    <header class="mb-4">
        <h1 class="display-4">{{ post.title }}</h1>
        <div class="post-meta text-muted">
            <i class="fas fa-user me-1"></i>
            {{ post.author.name }}
            <i class="fas fa-calendar ms-3 me-1"></i>
            {{ post.published_at|date('Y-m-d') }}
            <i class="fas fa-eye ms-3 me-1"></i>
            {{ post.views }} 阅读
        </div>
    </header>

    <div class="post-content">
        {{ post.content|safe }}
    </div>

    <div class="post-tags mt-4">
        {% for tag in post.tags %}
            <a href="{{ url_for('tag_posts', tag_slug=tag.slug) }}"
               class="badge bg-secondary me-1">
                #{{ tag.name }}
            </a>
        {% endfor %}
    </div>
</article>

<!-- 评论部分 -->
{% include 'blog/partials/comments.html' %}

<!-- 相关文章 -->
{% if related_posts %}
<div class="mt-5">
    

相关文章

<div class="row"> {% for related in related_posts %} <div class="col-md-6 mb-3"> <div class="card"> <div class="card-body"> <h5> <a href="{{ url_for('post_detail', slug=related.slug) }}"> {{ related.title }} </a> </h5> </div> </div> </div> {% endfor %} </div> </div> {% endif %} {% endblock %} {% block sidebar %} {{ super() }} <!-- 添加额外的侧边栏内容 --> <div class="card mt-4"> <div class="card-body"> <h5>目录</h5> <nav id="toc"></nav> </div> </div> {% endblock %} {% block extra_js %} {{ super() }} <script> // 生成目录 document.addEventListener('DOMContentLoaded', function() { const headings = document.querySelectorAll('.post-content h2, .post-content h3'); const toc = document.getElementById('toc'); headings.forEach((heading, index) => { const id = 'heading-' + index; heading.id = id; const link = document.createElement('a'); link.href = '#' + id; link.className = 'd-block py-1'; link.textContent = heading.textContent; if (heading.tagName === 'H3') { link.style.paddingLeft = '20px'; } toc.appendChild(link); }); }); </script> {% endblock %}
视图函数示例
from flask import render_template
from models import Post

@app.route('/blog/<slug>')
def post_detail(slug):
    """博客文章详情页"""
    post = Post.query.filter_by(slug=slug).first_or_404()

    # 增加阅读量
    post.increment_views()

    # 获取相关文章
    related_posts = Post.get_related_posts(post, limit=3)

    return render_template(
        'blog/post_detail.html',
        post=post,
        related_posts=related_posts,
        # 传递给include模板的变量
        show_comments=True,
        allow_commenting=post.allow_comments
    )

@app.route('/blog/category/<category_slug>')
def category_posts(category_slug):
    """分类文章列表页"""
    category = Category.query.filter_by(slug=category_slug).first_or_404()
    posts = Post.query.filter_by(category_id=category.id).paginate(page=1, per_page=10)

    return render_template(
        'blog/category.html',
        category=category,
        posts=posts,
        page_title=f"{category.name} - 文章分类"
    )
要点总结
  • 三层继承结构:base → blog_base → post_detail
  • 模块化设计:使用include引入公共组件
  • 灵活覆盖:子模板可以覆盖任何块
  • 变量传递:视图函数传递数据到模板
学习总结

通过本教程,你已经掌握了Flask模板继承的核心概念:

  • 基础模板:定义网站的基本框架结构
  • 子模板扩展:使用 {% extends %} 继承基础模板
  • 块覆盖:使用 {% block %} 覆盖父模板内容
  • 超级块:使用 {{ super() }} 引用父模板内容
  • 嵌套继承:创建多层模板继承结构
  • 包含与继承:合理使用 {% include %}{% extends %}
  • 最佳实践:遵循模板继承的最佳实践原则
  • 实战应用:在实际项目中应用模板继承
下一步学习建议
  • 学习Jinja2过滤器的高级用法
  • 掌握Flask表单处理与验证
  • 学习Flask蓝图的模块化开发
  • 了解Flask用户认证与授权
  • 实践Flask项目部署与优化