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 %} 把将来会变化的区域留出来。
命名提示
块名最好语义化,例如 title、extra_head、content、scripts,一看就知道作用,方便子模板理解和覆盖。
<!-- 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>© 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 内置了不少过滤器(例如 upper、truncate、length),但业务中常需要定制数据展示格式 —— 例如将时间转换成“年月日”,或按汉字字数截断文本。这时就需要自己写过滤器。
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. 小结与最佳实践
核心知识点回顾
避坑与最佳实践
- 继承层级 ≤ 3 层:超过三层维护成本急剧上升;
- 块名语义化:使用
content、extra_head、scripts 而非 block1、block2;
- 独立片段加下划线前缀:一方面语义清晰,另一方面 Flask 默认不直接服务以下划线开头的文件,更安全;
- 不滥用全局变量 / 函数:视图能通过上下文传入的小数据就交给视图,保持模板纯净;
- 日期、字符串处理优先用过滤器:避免在模板里塞一长串 Python 代码,可读性会变差。
现在你已经掌握了模板继承、组件包含、自定义过滤器和全局函数这四大法宝,足够应付绝大多数 Web 项目的模板架构需求。下一篇我们会深入 静态文件管理与前端资源整合,彻底打通前后端的协作通道,敬请期待。
🔗 扩展阅读