Flask 笔记六:模板继承,告别每个页面复制一遍 HTML

1. 这篇学完你能做什么

  • {% extends %} 继承一个 基础布局
  • {% block %} 在子页面里 只写变化的部分
  • 公共导航、Flash 提示 只维护一份
  • 静态 CSS 挂到布局里,全站生效

2. 为什么需要模板继承

第二篇、第三篇的页面可能是这样的:

<!-- note_list.html -->

<!DOCTYPE html>

<html>

<head>...</head>

<body>

<nav>...</nav>

<!-- 列表内容 -->

</body>

</html>

<!-- note_form.html -->

<!DOCTYPE html>

<html>

<head>...</head>

<body>

<nav>...</nav> <!-- 又写一遍 -->

<!-- 表单内容 -->

</body>

</html>

改一次导航,要改 N 个文件。

Flask 用的是 Jinja2 模板引擎,继承语法就是来解决这个问题的。


3. 先建一个「母版」:base.html

app/templates/ 下新建 base.html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<title>{% block title %}我的备忘录{% endblock %}</title>

<link rel="stylesheet" href="{{ url_for('static', filename='css/site.css') }}">

{% block css %}{% endblock %}

</head>

<body>

<header class="site-header">

<a href="{{ url_for('home.index') }}">首页</a>

<a href="{{ url_for('home.note_list') }}">备忘录</a>

<a href="{{ url_for('home.note_add') }}">新增</a>

</header>

<main class="site-main">

{% with messages = get_flashed_messages() %}

{% if messages %}

<ul class="flash-list">

{% for msg in messages %}

<li class="flash-item">{{ msg }}</li>

{% endfor %}

</ul>

{% endif %}

{% endwith %}

{% block content %}{% endblock %}

</main>

<footer class="site-footer">

<small>Flask 练习项目</small>

</footer>

{% block js %}{% endblock %}

</body>

</html>

几个关键点:

写法 作用
{% block title %} 子页面可改浏览器标题
{% block content %} 子页面填主要内容
{% block css %} / {% block js %} 个别页面额外引 CSS/JS
get_flashed_messages() Flash 提示写一次,全站都有

4. 子页面怎么写:列表页

app/templates/home/note_list.html 改成:

{% extends "base.html" %}

{% block title %}备忘录列表 · 我的备忘录{% endblock %}

{% block content %}

<h1>备忘录列表</h1>

<form method="get" action="{{ url_for('home.note_list') }}">

<input type="text" name="q" value="{{ q or '' }}" placeholder="搜标题或内容">

<button type="submit">搜索</button>

</form>

{% for note in page_data.items %}

<article class="note-item">

<h3>{{ note.title }}</h3>

<p>{{ note.content or '(无内容)' }}</p>

<a href="{{ url_for('home.note_edit', note_id=note.id) }}">编辑</a>

</article>

{% endfor %}

{% if page_data.has_prev %}

<a href="{{ url_for('home.note_list', page=page_data.prev_num, q=q) }}">上一页</a>

{% endif %}

{% if page_data.has_next %}

<a href="{{ url_for('home.note_list', page=page_data.next_num, q=q) }}">下一页</a>

{% endif %}

{% endblock %}

注意第一行:必须先 extends,再写 block。

子页面里 不用 再写 <html><head>、导航------母版已经有了。


5. 表单页同样继承

app/templates/home/note_form.html

{% extends "base.html" %}

{% block title %}{{ title }} · 我的备忘录{% endblock %}

{% block content %}

<h1>{{ title }}</h1>

<form method="post">

{{ form.csrf_token }}

<p>

{{ form.title.label }}<br>

{{ form.title(size=40) }}

{% for err in form.title.errors %}

<span class="error">{{ err }}</span>

{% endfor %}

</p>

<p>

{{ form.content.label }}<br>

{{ form.content(rows=5, cols=40) }}

</p>

<p>{{ form.submit }}</p>

<a href="{{ url_for('home.note_list') }}">返回列表</a>

</form>

{% endblock %}

新增页和编辑页 共用一个模板,和第三篇一样;

区别只是母版帮你包好了外壳。


6. 静态文件挂到布局里

第五篇之前,CSS 可能散落在各个页面。

现在只在 base.html 引一次:

<link rel="stylesheet" href="{{ url_for('static', filename='css/site.css') }}">

文件放在 app/static/css/site.css

Flask 会自动从 static 目录提供静态资源。

某个页面需要额外样式时,用 {% block css %}

{% extends "base.html" %}

{% block css %}

<link rel="stylesheet" href="{{ url_for('static', filename='css/note-chart.css') }}">

{% endblock %}

{% block content %}

...

{% endblock %}


7. 多层继承(了解即可)

项目变大后,可以 母版套母版:

base.html ← 全站:head、footer

└── admin_base.html ← 后台:侧栏、面包屑

└── note_list.html

admin_base.html

{% extends "base.html" %}

{% block content %}

<aside class="admin-sidebar">...</aside>

<section class="admin-content">

{% block admin_content %}{% endblock %}

</section>

{% endblock %}

具体列表页:

{% extends "admin/admin_base.html" %}

{% block admin_content %}

<h1>备忘录管理</h1>

...

{% endblock %}

入门阶段一层 base.html 就够;

知道可以嵌套,以后拆 home / admin 两套布局时会用到。


8. includeextends 的区别

语法 适合什么
{% extends %} 整页有统一骨架,子页填 block
{% include "xxx.html" %} 抽一小段复用,如分页条、删除按钮

分页条可以单独做成 includes/pagination.html

{# includes/pagination.html #}

{% if page_data.has_prev %}

<a href="{{ url_for(endpoint, page=page_data.prev_num, q=q) }}">上一页</a>

{% endif %}

列表页里:

{% include "includes/pagination.html" %}

规则:整页结构用 extends,小组件用 include。


9. 一张图串起来

浏览器请求 /notes/

render_template("home/note_list.html", ...)

note_list.html {% extends "base.html" %}

base.html 输出完整 HTML

├─ head + site.css

├─ 导航

├─ Flash 区域

├─ block content ← 列表内容填在这里

└─ footer


10. 新手常踩的 4 个坑

坑 1:extends 不在文件最前面

{% extends %} 必须是模板 第一行(前面不能有空行以外的内容),否则报错。

坑 2:block 名字对不上

母版是 {% block content %},子页面写成 {% block main %},内容不会显示。

名字必须一致。

坑 3:子页面又写了一遍 <html>

继承后子页面 只写 block 里的片段,不要再包一层完整 HTML。

坑 4:Flash 每个页面各写一份

应放在 base.html 里 写一次;

个别页面要分类提示可以用 get_flashed_messages(with_categories=true),第六篇先知道有这能力即可。