🕐 预计用时:2-3 小时 | 🎯 目标:掌握模板继承、宏、自定义过滤器和静态文件管理
昨天我们写了第一个模板,但有个明显的问题——每个页面都写了完整的 HTML 结构:
<!-- home.html -->
<html><head><title>首页</title></head>
<body><nav>...导航栏...</nav> 页面内容 </body></html>
<!-- about.html -->
<html><head><title>关于</title></head>
<body><nav>...导航栏...</nav> 页面内容 </body></html>
<!-- contact.html -->
<html><head><title>联系</title></head>
<body><nav>...导航栏...</nav> 页面内容 </body></html>想象你有 100 个页面,老板说"把导航栏的 Logo 换一下"——你要改 100 个文件!这简直是维护噩梦。
💡 模板继承 = 印章 + 填空题
你刻一个"印章"(base.html),上面有网站的通用结构:HTML骨架、导航栏、页脚。每个子页面只需要"填空"——在指定位置填入自己的独特内容。
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}我的网站{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<!-- 导航栏(所有页面共享) -->
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
<a href="/contact">联系</a>
</nav>
<!-- 主内容区(每个页面不同) -->
<main>
{% block content %}{% endblock %}
</main>
<!-- 页脚(所有页面共享) -->
<footer>
<p>© 2026 我的网站</p>
</footer>
</body>
</html><!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}首页 - 我的网站{% endblock %}
{% block content %}
<h1>🏠 欢迎来到首页</h1>
<p>这是首页内容,只写独特部分就行!</p>
{% endblock %}<!-- templates/about.html -->
{% extends "base.html" %}
{% block title %}关于我们{% endblock %}
{% block content %}
<h1>📖 关于我们</h1>
<p>我们是一个热爱 Python 的团队。</p>
{% endblock %}看到神奇之处了吗?子模板只需要写"填空题"的部分,导航栏、页脚、HTML骨架全部来自 base.html。
继承可以多层叠加,就像家族族谱:
base.html ← 爷爷(HTML骨架 + 导航 + 页脚)
└── base_user.html ← 爸爸(继承爷爷 + 用户中心侧边栏)
└── profile.html ← 孙子(继承爸爸 + 个人资料内容)
└── settings.html ← 孙子(继承爸爸 + 设置内容)<!-- templates/base_user.html -->
{% extends "base.html" %}
{% block content %}
<div class="user-layout">
<aside>
<a href="/profile">个人资料</a>
<a href="/settings">设置</a>
</aside>
<div class="user-main">
{% block user_content %}{% endblock %}
</div>
</div>
{% endblock %}<!-- templates/profile.html -->
{% extends "base_user.html" %}
{% block user_content %}
<h2>个人资料</h2>
<p>用户名:{{ user.name }}</p>
{% endblock %}{# 1. 基础定义(空块,子模板填充) #}
{% block content %}{% endblock %}
{# 2. 带默认内容(子模板可选择覆盖或使用默认) #}
{% block sidebar %}
<p>这是默认侧边栏内容</p>
{% endblock %}
{# 3. 使用 super() 保留父模板内容并追加 #}
{% block content %}
{{ super() }} {# 保留父模板的 content 块 #}
<p>这是追加的内容</p>
{% endblock %}💡 super() 就像"追加"而非"覆盖":
• 不用 super() → 覆盖父模板的块内容
• 用 super() → 保留父模板的块内容 + 追加新内容
{# ❌ 不能在同一个模板中定义两次同名 block #}
{% block title %}首页{% endblock %}
{% block title %}关于{% endblock %} {# 错误! #}
{# ❌ 不能在 if/for 内定义 block #}
{% if user %}
{% block content %}...{% endblock %} {# 错误! #}
{% endif %}宏 = 模板里的函数。 如果你有一段 HTML 代码需要重复使用(比如按钮、表单字段、卡片),把它定义为宏,然后像函数一样调用。
{# 定义宏:就像定义一个函数 #}
{% macro input(name, label, type='text', placeholder='') %}
<div class="form-group">
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type }}"
name="{{ name }}"
id="{{ name }}"
placeholder="{{ placeholder }}">
</div>
{% endmacro %}
{# 使用宏:就像调用函数 #}
<form>
{{ input('username', '用户名', placeholder='请输入用户名') }}
{{ input('password', '密码', type='password') }}
{{ input('email', '邮箱', type='email') }}
</form>宏定义放在模板里不够优雅,可以单独放在一个文件中,然后导入:
<!-- templates/macros/forms.html -->
{% macro input(name, label, type='text') %}
<div class="form-group">
<label>{{ label }}</label>
<input type="{{ type }}" name="{{ name }}">
</div>
{% endmacro %}
{% macro textarea(name, label, rows=5) %}
<div class="form-group">
<label>{{ label }}</label>
<textarea name="{{ name }}" rows="{{ rows }}"></textarea>
</div>
{% endmacro %}
{% macro submit(text='提交') %}
<button type="submit" class="btn">{{ text }}</button>
{% endmacro %}{# 在其他模板中导入 #}
{% from "macros/forms.html" import input, textarea, submit %}
<form>
{{ input('title', '标题') }}
{{ textarea('content', '内容') }}
{{ submit('发布文章') }}
</form>💡 宏 vs include 的区别:
• 宏(macro):可传参数的"函数",适合生成动态组件
• include:直接插入一段固定的模板片段,不传参数
类比编程:宏 = 函数(有参数),include = 复制粘贴(无参数但带上下文)。
内置过滤器不够用时——比如你想把数字格式化成"万"为单位,或者把 Markdown 转成 HTML。
# app.py
@app.template_filter('reverse')
def reverse_filter(s):
"""反转字符串"""
return s[::-1]
@app.template_filter('time_ago')
def time_ago_filter(dt):
"""时间距今多久"""
from datetime import datetime
diff = datetime.now() - dt
if diff.days > 365:
return f'{diff.days // 365} 年前'
elif diff.days > 30:
return f'{diff.days // 30} 个月前'
elif diff.days > 0:
return f'{diff.days} 天前'
elif diff.seconds > 3600:
return f'{diff.seconds // 3600} 小时前'
elif diff.seconds > 60:
return f'{diff.seconds // 60} 分钟前'
else:
return '刚刚'
@app.template_filter('currency')
def currency_filter(amount):
"""格式化货币"""
return f'¥{amount:,.2f}'<!-- 在模板中使用 -->
<p>{{ "Hello" | reverse }}</p> <!-- olleH -->
<p>{{ post_date | time_ago }}</p> <!-- 3 天前 -->
<p>{{ 12345.6 | currency }}</p> <!-- ¥12,345.60 -->{# 过滤器可以链式调用,从左到右依次执行 #}
{{ " Hello World " | trim | upper }} <!-- HELLO WORLD -->
{{ "hello world" | title | truncate(8) }} <!-- Hello... -->
{{ items | sort | join(', ') }} <!-- a, b, c -->upper | {{ "hi" | upper }} | ||
lower | {{ "HI" | lower }} | ||
title | {{ "hi there" | title }} | ||
capitalize | {{ "hi there" | capitalize }} | ||
trim | {{ " hi " | trim }} | ||
length | {{ "hello" | length }} | ||
default(v) | {{ x | default('N/A') }} | ||
join(sep) | {{ ['a','b'] | join('-') }} | ||
sort | {{ [3,1,2] | sort }} | ||
reverse | {{ [1,2,3] | reverse }} | ||
first | {{ [1,2,3] | first }} | ||
last | {{ [1,2,3] | last }} | ||
unique | {{ [1,2,1] | unique }} | ||
truncate(n) | {{ "hello world" | truncate(5) }} | ||
striptags | {{ "<b>hi</b>" | striptags }} | ||
escape | {{ "<script>" | escape }} | ||
safe | {{ html_content | safe }} |
⚠️ 关于 safe 过滤器的安全警告:{{ user_input | safe }} 会直接渲染用户输入的 HTML,这可能导致 XSS 攻击(跨站脚本攻击)。
只有当你100%确定内容是安全的(比如你自己写的 HTML)时才用 safe。对用户输入永远不要用 safe!
CSS 样式表、JavaScript 脚本、图片、字体——这些不需要 Python 处理的文件就是静态文件。
# 项目结构
myapp/
├── app.py
├── templates/
│ └── index.html
└── static/ ← 静态文件都放这里
├── css/
│ └── style.css
├── js/
│ └── main.js
└── images/
└── logo.png<!-- 引用 CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- 引用 JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<!-- 引用图片 -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
<!-- favicon -->
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">💡 为什么用 url_for 而不是直接写路径?url_for('static', filename='css/style.css') 会自动生成正确的路径,即使你的应用部署在子目录下(如 /myapp/static/css/style.css)。
直接写 /static/css/style.css 在开发环境可能没问题,但部署到生产环境时可能会坏掉。用 url_for 是最佳实践。
/* static/css/style.css */
body {
font-family: -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
nav {
background: #07c160;
padding: 12px 20px;
border-radius: 8px;
}
nav a {
color: white;
text-decoration: none;
margin-right: 16px;
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
margin: 16px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}include 就是把另一个模板的内容"粘贴"到当前位置。 适合复用不需要传参的模板片段。
{# 直接插入导航栏模板 #}
{% include 'navbar.html' %}
{# 插入侧边栏 #}
{% include 'sidebar.html' %}
{# 带上下文的包含(可以访问当前模板的变量) #}
{% include 'user_card.html' with context %}
{# 忽略缺失的模板(不报错) #}
{% include 'optional_widget.html' ignore missing %}| 继承 extends | |||
| 宏 macro | |||
| include |
{# 导航栏适合用 include(不需要传参) #}
{% include 'partials/navbar.html' %}
{# 表单字段适合用宏(需要传 name、label 等参数) #}
{% from 'macros/forms.html' import input %}
{{ input('username', '用户名') }}
{# 整个页面结构适合用继承 #}
{% extends "base.html" %}比如网站名称、当前用户、导航菜单——每个页面都需要,难道每个视图函数都要传一遍?
上下文处理器(context_processor) 解决了这个问题:
# app.py
@app.context_processor
def inject_globals():
"""这些变量会自动注入到所有模板中"""
return {
'site_name': 'Python 学习网',
'current_year': 2026,
'nav_items': [
{'url': '/', 'text': '首页'},
{'url': '/about', 'text': '关于'},
{'url': '/blog', 'text': '博客'},
]
}<!-- 所有模板中都可以直接使用这些变量 -->
<title>{{ site_name }}</title>
<nav>
{% for item in nav_items %}
<a href="{{ item.url }}">{{ item.text }}</a>
{% endfor %}
</nav>
<footer>© {{ current_year }} {{ site_name }}</footer>Flask 的 g 对象是一个请求级别的临时存储,在一次请求的生命周期内有效:
from flask import g
@app.before_request
def before_request():
"""每次请求前执行"""
g.user = get_current_user() # 存到 g 中
g.db = get_db_connection()
@app.route('/dashboard')
def dashboard():
# 在视图函数中使用 g 中的数据
return render_template('dashboard.html', user=g.user)💡 g vs session 的区别:
• g:仅在当前请求内有效,请求结束就没了。适合存数据库连接等。
• session:跨请求有效(存在 Cookie 里)。适合存用户登录状态等。
类比:g = 便利贴(用完就扔),session = 记事本(长期保存)。
# 要求:
# 1. 创建 base.html:包含导航栏、content block、页脚
# 2. 创建 index.html:继承 base.html,显示首页内容
# 3. 创建 about.html:继承 base.html,显示关于页面
# 4. 导航栏中当前页面的链接高亮显示(用 block 或变量控制)# 要求:
# 创建一个卡片宏 card(title, content, footer='')
# 渲染效果:
# ┌─────────────────────┐
# │ 📌 标题 │
# │ │
# │ 内容正文 │
# │ │
# │ 底部信息(可选) │
# └─────────────────────┘
# 使用这个宏渲染 3 张不同的卡片# 要求:
# 1. @app.template_filter('mask') → 邮箱脱敏
# "test@example.com" → "t***@example.com"
# 2. @app.template_filter('word_count') → 统计字数
# "hello world" → 2
# 3. @app.template_filter('file_size') → 文件大小格式化
# 1536 → "1.5 KB"
# 1048576 → "1.0 MB"🚀 明日预告:Day 53 — 表单处理
今天你学会了优雅地组织模板,但还没有学会接收用户输入。明天我们会学 WTForms——Flask 最强大的表单处理库。从文本框到文件上传,从数据验证到 CSRF 防护,让你的网站从"只读"变成"可交互"!