Jinja2 模板引擎(下):模板继承、include 与宏

📂 所属阶段:第一阶段 — 破冰启航(基础篇)
🔗 相关章节:Jinja2 模板引擎(上) · 静态文件管理


1. 模板继承(最重要!)

1.1 为什么需要继承?

没有继承:每个页面都复制导航栏 + 页脚 + CSS → 修改一次改 N 个文件 😱

有继承:   base.html 定义骨架 → 只需填充内容 → 改一处全站生效 ✅

1.2 父模板(基础模板)

<!-- 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>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    {% block extra_head %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar">
        <a href="{{ url_for('index') }}">首页</a>
        <a href="{{ url_for('articles') }}">文章</a>
        <a href="{{ url_for('about') }}">关于</a>
        {% if current_user.is_authenticated %}
            <a href="{{ url_for('profile') }}">{{ current_user.name }}</a>
            <a href="{{ url_for('logout') }}">退出</a>
        {% else %}
            <a href="{{ url_for('login') }}">登录</a>
        {% endif %}
    </nav>

    <!-- Flash 消息 -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            <div class="flash-messages">
                {% for category, message in messages %}
                    <div class="alert alert-{{ category }}">{{ message }}</div>
                {% endfor %}
            </div>
        {% endif %}
    {% endwith %}

    <!-- 页面主内容 -->
    <main class="container">
        {% block content %}{% endblock %}
    </main>

    <!-- 页脚 -->
    <footer>
        <p>&copy; 2026 道满博客 | <a href="https://www.daomanpy.com">道满 Python AI</a></p>
    </footer>

    {% block scripts %}{% endblock %}
</body>
</html>

1.3 子模板(继承并覆盖)

<!-- templates/articles/index.html -->
{% extends "base.html" %}

{% block title %}文章列表 - 道满博客{% endblock %}

{% block extra_head %}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/articles.css') }}">
{% endblock %}

{% block content %}
    <h1>文章列表</h1>

    {% for article in articles %}
        <article class="article-card">
            <h2><a href="{{ url_for('article', id=article.id) }}">{{ article.title }}</a></h2>
            <div class="meta">
                <span>作者:{{ article.author.name }}</span>
                <span>发布时间:{{ article.created_at.strftime('%Y-%m-%d') }}</span>
                <span>阅读:{{ article.views }}</span>
            </div>
            <p>{{ article.summary }}</p>
        </article>
    {% else %}
        <p>暂无文章,去 <a href="{{ url_for('create_article') }}">发布</a> 第一篇吧!</p>
    {% endfor %}

    <!-- 分页 -->
    {% if pagination %}
        <div class="pagination">
            {% if pagination.has_prev %}
                <a href="{{ url_for('articles', page=pagination.prev_num) }}">上一页</a>
            {% endif %}
            <span>第 {{ pagination.page }} / {{ pagination.pages }} 页</span>
            {% if pagination.has_next %}
                <a href="{{ url_for('articles', page=pagination.next_num) }}">下一页</a>
            {% endif %}
        </div>
    {% endif %}
{% endblock %}

1.4 三层继承(更复杂的项目)

templates/
├── base.html              ← 顶层:HTML 骨架(所有页面共享)
├── _layout.html           ← 中层:布局(侧边栏等结构)
├── articles/
│   ├── _article_layout.html  ← 子布局:文章相关页面结构
│   ├── index.html         ← 文章列表页
│   ├── article.html       ← 文章详情页
│   └── editor.html        ← 文章编辑器页

2. include 包含

2.1 include 的用法

<!-- templates/_sidebar.html -->
<div class="sidebar">
    <h3>热门文章</h3>
    <ul>
        {% for article in hot_articles %}
            <li><a href="{{ url_for('article', id=article.id) }}">{{ article.title }}</a></li>
        {% endfor %}
    </ul>
</div>

<!-- templates/index.html -->
{% extends "base.html" %}

{% block content %}
    <div class="main-content">
        <!-- 渲染文章列表 -->
        {{ content }}
    </div>

    <!-- 包含侧边栏 -->
    {% include "_sidebar.html" %}
{% endblock %}

2.2 include 与继承的区别

{% include %} → 把另一个模板的内容原样插入(没有继承关系)
{% extends %} → 以另一个模板为基础,重写特定 block(继承关系)

3. set 变量赋值

3.1 模板内变量

{% set navigation = [
    ('/', '首页'),
    ('/articles', '文章'),
    ('/about', '关于')
] %}

<ul>
{% for href, label in navigation %}
    <li><a href="{{ href }}">{{ label }}</a></li>
{% endfor %}
</ul>

<!-- endset 块赋值 -->
{% set content %}
    这里可以放很长的内容。
    {{ user.name }} 的个人简介。
{% endset %}

4. 自定义过滤器

4.1 注册自定义过滤器

# app/utils.py
from flask import Flask

app = Flask(__name__)

# 方式一:装饰器
@app.template_filter("datetime")
def format_datetime(dt, fmt="%Y-%m-%d %H:%M"):
    if dt is None:
        return ""
    return dt.strftime(fmt)

@app.template_filter("truncate_words")
def truncate_words(text, num_words=50):
    words = text.split()
    if len(words) <= num_words:
        return text
    return " ".join(words[:num_words]) + "..."

# 方式二:add_template_filter
app.add_template_filter(my_filter, "filter_name")

4.2 使用自定义过滤器

<p>发布时间:{{ article.created_at|datetime }}</p>
<p>摘要:{{ article.content|truncate_words(30) }}</p>

5. 自定义全局函数

5.1 注册全局函数

# app/utils.py

def get_articles_count():
    """获取文章总数"""
    from app.models import Article
    return Article.query.count()

def get_site_name():
    return "道满博客"

# 注册
app.jinja_env.globals["articles_count"] = get_articles_count
app.jinja_env.globals["site_name"] = get_site_name

5.2 在模板中使用

<p>全站共 {{ articles_count() }} 篇文章</p>
<p>{{ site_name() }}</p>

6. Flask 模板内置全局变量

<!-- 无需传入模板,Jinja2 自动提供 -->

{{ config }}           ← Flask 配置对象
{{ request }}           ← 当前请求对象
{{ session }}           ← 会话对象
{{ g }}                 ← 请求级全局对象
{{ url_for() }}         ← URL 反向生成
{{ get_flashed_messages() }}  ← Flash 消息

{{ current_user }}      ← Flask-Login 当前用户
{{ current_user.is_authenticated }}  ← 是否登录

7. 小结

<!-- 模板继承三步走 -->

{# 1. 父模板定义可覆盖的块 #}
{% block content %}{% endblock %}

{# 2. 子模板声明继承 #}
{% extends "base.html" %}

{# 3. 子模板覆盖块内容 #}
{% block content %}
    <p>这是页面特有内容</p>
{% endblock %}

💡 最佳实践:模板继承层级不要超过 3 层,否则维护困难。每个 block 尽量只包含一类内容(title、head、content、scripts)。


🔗 扩展阅读