Jinja2 模板引擎(下):复用与扩展的核心利器

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


上一篇我们啃下了 Jinja2 的变量注入、循环控制、分支判断三板斧,已经能写出单页面的动态渲染。但想要搭一个有登录状态、页脚、侧边栏的完整网站,还差一步 —— 复用。如果照着 index.html 把导航栏、页脚复制粘贴到十几个页面,等哪天改个版权年份简直就是灾难 😤。

这篇就来解锁 复用与扩展 的核心玩法:模板继承、片段包含、自定义过滤器与全局函数,配合三层继承技巧和内置变量,足够撑起生产环境的模板架构。


1. 模板继承:最常用的复用方案 ✨

1.1 为什么用继承?别再复制粘贴了!

先看两种项目结构的对比,感受一下“没继承”和“有继承”的区别:

❶ 无继承 —— 噩梦模式
templates/
├── index.html      ← 复制了导航 + 页脚
├── article.html    ← 又复制了导航 + 页脚
├── profile.html    ← 还在复制导航 + 页脚
└── ...             (全站 10 个页面,改一次页脚就要改 10 个文件)

:::success ❷ 有继承 —— 丝滑模式

templates/
├── base.html       ← 只写一次导航 + 页脚 + 基础 CSS
├── index.html      ← 只写首页特有内容
├── article.html    ← 只写文章页特有内容
└── ...             (改一次页脚,base.html 里动一刀全站生效)

:::

模板继承 的核心是:把网站的公共骨架抽取到一个父模板里,用块(block)标记出子页面可以替换或填充的区域。

1.2 先搭「父模板」:定义共享骨架

父模板通常叫 base.html,负责整个网站的 HTML 基础结构、公共导航、页脚、全局引入的 CSS/JS 等。我们需要用 {% block 块名 %}{% endblock %} 把将来会变化的区域留出来。

命名提示

块名最好语义化,例如 titleextra_headcontentscripts,一看就知道作用,方便子模板理解和覆盖。

<!-- 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') }}">
    <!-- ✅ 页面专属 CSS / Meta 预留块 -->
    {% block extra_head %}{% endblock %}
</head>
<body>
    <!-- 固定导航栏,借助 Flask-Login 的 current_user 展示状态 -->
    <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>

    <!-- ✅ 页面专属 JS 预留块 -->
    {% block scripts %}{% endblock %}
</body>
</html>

父模板里 block 内的内容就是默认值。如果子模板不覆盖,就显示默认值;覆盖了就用子模板的内容。

1.3 再写「子模板」:填充 / 覆盖特定块

子模板必须第一行{% extends "父模板路径" %} 声明继承关系,然后只覆盖自己关心的 block不要在块以外写任何多余的 HTML,否则 Jinja2 会报错。

如何保留父块内容?

如果希望在父块原来的内容上追加自己的东西,可以在子块的开始处调用 {{ super() }}。例如子模板要在通用导航后再加一个搜索框,就可以覆盖导航块并先写上 {{ super() }},然后追加自己的搜索表单。

下面写一个文章列表页的子模板,感受一下继承的清爽:

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

<!-- 1. 覆盖标题 -->
{% block title %}文章列表 - 道满博客{% endblock %}

<!-- 2. 补充文章页专属样式 -->
{% block extra_head %}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/articles.css') }}">
{% endblock %}

<!-- 3. 填充主内容区 -->
{% 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 %}

现在你只管写页面真正不同的部分,导航、页脚、错误提示这些杂活全部由 base.html 承包。

1.4 进阶:三层继承(复杂项目的救星)

当网站发展到有多个模块(比如「文章模块」「用户模块」),每个模块内部又有自己的共享侧边栏或子导航时,单一的 base.html 就不够了。这时可以引入中层子模板,形成三层继承结构:

templates/
├── base.html                ← 顶层:整个站点的 HTML 骨架
├── _layout_articles.html   ← 中层:文章模块专属骨架(统一侧边栏、搜索框)
├── _layout_users.html      ← 中层:用户模块专属骨架(统一个人卡片)
├── articles/
│   ├── index.html          ← 继承 _layout_articles
│   ├── detail.html         ← 继承 _layout_articles
│   └── editor.html         ← 继承 _layout_articles
└── users/
    └── profile.html         ← 继承 _layout_users

中层模板同样使用 {% extends "base.html" %},在自己的 block 里再次划分粒度更细的块,供最终的页面模板继续覆盖。

避坑提醒

继承层级建议不超过 3 层,否则追踪块从哪里来、覆盖了谁会变得非常头疼。


2. include:独立片段的“插入器” 📎

2.1 什么时候用 include?

{% include %} 与继承是互补关系。它用来原封不动地插入一个独立的小模板片段,适合:

  • 多个页面重复使用的小功能组件:热门文章侧边栏、评论区、分享按钮;
  • 不需要覆盖、没有继承关系的纯内容块

2.2 include 的用法

先把独立片段抽离出来,文件名通常加上下划线 _ 前缀,提示这是一个“部件”而不是完整页面:

<!-- templates/_hot_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="row">
        <div class="col-8">
            <h1>最新动态</h1>
            <!-- 主要内容 -->
        </div>
        <div class="col-4">
            <!-- 直接插入热门侧边栏 -->
            {% include "_hot_sidebar.html" %}
        </div>
    </div>
{% endblock %}

这样做的好处是:改一次热门文章组件的样式或逻辑,所有引用它的页面都会同步更新。


3. 自定义过滤器:加工数据的“小工具” 🔧

Jinja2 内置了不少过滤器(例如 uppertruncatelength),但业务中常需要定制数据展示格式 —— 例如将时间转换成“年月日”,或按汉字字数截断文本。这时就需要自己写过滤器。

3.1 注册自定义过滤器

以 Flask 为例,在应用工厂或模块文件中注册即可。推荐两种方式:

# app.py
from flask import Flask
from datetime import datetime

app = Flask(__name__)

# 方式一:装饰器注册(推荐,简洁直观)
@app.template_filter("custom_date")
def format_custom_date(dt, fmt="%Y年%m月%d日 %H:%M"):
    """自定义日期显示格式"""
    if not dt:
        return ""
    # 兼容字符串格式的时间(视业务需要)
    if isinstance(dt, str):
        try:
            dt = datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
        except:
            return ""
    return dt.strftime(fmt)

@app.template_filter("truncate_chinese")
def truncate_chinese(text, max_len=50, suffix="..."):
    """中文友好的截断(按字符长度,英文单词兼容)"""
    if not text:
        return ""
    if len(text) <= max_len:
        return text
    return text[:max_len] + suffix

# 方式二:add_template_filter(适合批量注册)
def double(value):
    return value * 2
app.add_template_filter(double, "double")

3.2 在模板中使用

跟内置过滤器一样,通过 | 管道调用,需要传参时加上括号:

<p>发布时间:{{ article.created_at|custom_date }}</p>
<p>摘要:{{ article.content|truncate_chinese(100, " [查看全文]") }}</p>
<p>双倍数字:{{ 5|double }}</p>

过滤器是“小工具”,应该保持单一职责,只做数据展示层的转换,不要在里面写复杂的业务逻辑。


4. 自定义全局函数:随时调用的“全局助手” 🛠️

如果你想在模板中直接调用一个需要动态计算、接受参数的函数(例如获取当前用户未读消息数、获取热门标签),但又不想在每个视图函数里都手动查询再传入,那就该自定义全局函数上场了。

4.1 注册全局函数

全局函数注册到 app.jinja_env.globals 字典中,模板中直接当普通函数调用:

# app.py
from app.models import Tag

def get_hot_tags(limit=5):
    """返回指定数量的热门标签(假设按文章数量排序)"""
    return Tag.query.order_by(Tag.article_count.desc()).limit(limit).all()

def get_site_name():
    """从配置中返回网站名"""
    return app.config.get("SITE_NAME", "道满博客")

# 注册到全局命名空间
app.jinja_env.globals["hot_tags"] = get_hot_tags
app.jinja_env.globals["site_name"] = get_site_name

4.2 在模板中使用

直接在模板里调用即可,不需要通过视图传递:

<!-- 页面标题可直接引用全局函数 -->
<title>{% block title %}{{ site_name() }}{% endblock %}</title>

<!-- 在侧边栏动态获取热门标签 -->
<div class="sidebar">
    <h3>🏷️ 热门标签</h3>
    <div class="tags">
        {% for tag in hot_tags(8) %}
            <a href="{{ url_for('tag_articles', tag_id=tag.id) }}">{{ tag.name }}</a>
        {% endfor %}
    </div>
</div>

使用全局函数时要注意:

  • 不要在全局函数里做复杂计算或查重数据库避免性能问题,尽量利用缓存或合理的查询。
  • 视图能直接提供的小数据就不要做成全局的,避免模板层耦合业务逻辑。

5. 小结与最佳实践

核心知识点回顾

功能适用场景关键语法 / 方式
模板继承全站 / 模块级别共享大骨架{% extends %}, {% block %}, {{ super() }}
include 包含独立小功能组件(侧边栏等){% include "_xxx.html" %}
自定义过滤器对单条数据做展示格式转换@app.template_filter(), {{ value|filter(arg) }}
自定义全局函数动态计算全局数据(标签、配置)app.jinja_env.globals["name"] = func

避坑与最佳实践

  1. 继承层级 ≤ 3 层:超过三层维护成本急剧上升;
  2. 块名语义化:使用 contentextra_headscripts 而非 block1block2
  3. 独立片段加下划线前缀:一方面语义清晰,另一方面 Flask 默认不直接服务以下划线开头的文件,更安全;
  4. 不滥用全局变量 / 函数:视图能通过上下文传入的小数据就交给视图,保持模板纯净;
  5. 日期、字符串处理优先用过滤器:避免在模板里塞一长串 Python 代码,可读性会变差。

现在你已经掌握了模板继承、组件包含、自定义过滤器和全局函数这四大法宝,足够应付绝大多数 Web 项目的模板架构需求。下一篇我们会深入 静态文件管理与前端资源整合,彻底打通前后端的协作通道,敬请期待。


🔗 扩展阅读