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

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


上一篇我们啃下了Jinja2的变量注入、循环控制、分支判断三板斧,能写单页面渲染了。但要做有登录/页脚/侧边栏的完整网站,复制粘贴一套导航改N次可太笨了😤

这篇就解锁复用、扩展的核心玩法:模板继承、片段包含、自定义功能(过滤器+全局函数),配合三层继承/内置变量,直接能用做生产级模板的基础。


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

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

先看痛点对比,比文字更直观:

❶ 无继承的噩梦
templates/
├── index.html(复制粘贴了导航+页脚)
├── article.html(复制粘贴了导航+页脚)
├── profile.html(复制粘贴了导航+页脚)
└── ...(全站10个页面,改一次页脚版权号要找10个文件)

:::success ❷ 有继承的爽感

templates/
├── base.html(只写1次导航+页脚+基础CSS)
├── index.html(只写首页特有内容)
├── article.html(只写文章特有内容)
└── ...(全站改一次页脚,base.html搞定)

:::

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

父模板(通常叫 base.html)就是整个网站的“空壳”——用 {% 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,子模板可覆盖 -->
    <title>{% block title %}道满博客{% endblock %}</title>
    <!-- 全站通用CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
    <!-- ✅ 页面专属CSS/meta预留块 -->
    {% 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>
        <!-- 登录状态展示用Flask-Login的current_user -->
        {% 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>

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

子模板必须第一行{% extends "父模板路径" %} 声明继承关系,之后只能覆盖/补充父模板的 block,不能写块外的零散内容。

补充父模板内容

如果子模板想保留父模板的块内容,再加自己的东西,可以用 {{ super() }}。比如子模板想在通用导航后面加一个「文章搜索框」,就可以覆盖 navbar 块,开头先放 {{ super() }}

这里写一个文章列表页的子模板:

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

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

<!-- 2. 补充文章页专属CSS -->
{% 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 %}

    <!-- 4. 补充简单分页 -->
    {% 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_articles.html  ← 中层:文章模块的专属骨架(加侧边栏、搜索框)
├── _layout_users.html     ← 中层:用户模块的专属骨架(加个人信息卡片)
├── articles/
│   ├── index.html         ← 继承_layout_articles:文章列表
│   ├── detail.html        ← 继承_layout_articles:文章详情
│   └── editor.html        ← 继承_layout_articles:文章编辑
└── users/
    └── profile.html       ← 继承_layout_users:个人资料
避坑提醒

继承层级最多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自带了一些过滤器(比如 uppertruncate),但如果需要定制数据格式(比如自定义日期显示、字数截断(单词/中文分开算)、敏感词替换),就得自己写。

3.1 注册自定义过滤器(两种方式)

以Flask为例,注册在 app.py 或单独的 utils.py 里都行:

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

app = Flask(__name__)

# 方式一:装饰器(更简洁,推荐)
# filter_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="..."):
    """中文友好的截断(英文单词不算1个字符)"""
    if not text:
        return ""
    # 这里简化处理,实际可以用 jieba 分词更准确
    if len(text) <= max_len:
        return text
    return text[:max_len] + suffix

# 方式二:add_template_filter(适合批量注册)
def my_another_filter(value):
    return value * 2
app.add_template_filter(my_another_filter, "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.py
from app.models import Article, 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", "道满博客")

# 注册全局函数(jinja_env.globals)
app.jinja_env.globals["hot_tags"] = get_hot_tags
app.jinja_env.globals["site_name"] = get_site_name

4.2 在模板中使用

直接调用就行,不用传任何视图里的变量:

<!-- 模板顶部的title也可以用全局变量 -->
<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
自定义全局函数动态计算全局数据app.jinja_env.globals["name"] = func

避坑与最佳实践

  1. 继承层级≤3层:多了难维护
  2. 块名语义化contentextra_headblock1block2
  3. 独立片段加下划线:防止被路由直接访问(Flask默认不访问下划线开头的文件,但养成习惯)
  4. 全局变量/函数不要滥用:视图能传的小数据就不要做成全局的
  5. 日期/数字处理优先用过滤器:不要在模板里写复杂的Python代码

🔗 扩展阅读