#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>© 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)。
🔗 扩展阅读

