Python · 5 分钟阅读
3. Flask 请求-响应循环
目录
- 1. 为什么需要模板
- 2. 最小模板示例
- 3. 渲染模板:
render_template() - 4. 模板语法速查
- 5. 控制流示例
- 6. 模板继承:避免重复 HTML
- 7. 链接与静态资源:
url_for - 8. 把数据传给模板
- 9. 完整可运行示例
- 10. 常见问题
- 小结
视图函数的核心职责是生成响应。但当响应是一段复杂的 HTML 时,把 HTML 写在
Python 字符串里会非常痛苦。Flask 使用 Jinja2
作为模板引擎,把表现层从业务层中分离出来。
1. 为什么需要模板
把业务逻辑和表现逻辑混在一起,代码很快会变成这样:
# 反例:HTML 拼字符串
return "<h1>Hello, " + name + "!</h1>"
一旦 HTML 复杂起来,这种写法既难写、也难维护、更难做转义防 XSS。模板把“长什么样”
交给 HTML 文件,Python 只负责“传什么数据”。
2. 最小模板示例
约定:所有模板放在 templates/ 目录下(与 app.py 同级)。
myapp/
├── app.py
└── templates/
├── index.html
└── user.html
<!-- templates/index.html -->
<h1>Hello, World!</h1>
<!-- templates/user.html -->
<h1>Hello, {{ name }}!</h1>
{{ name }} 是 Jinja2 的变量表达式,渲染时会被替换成 Python 传入的值。
Jinja2 默认会对
{{ }}中的值进行 HTML 转义,所以直接传用户输入是安全的。
3. 渲染模板:render_template()
from flask import Flask, render_template
app = Flask(__name__)
@app.get("/")
def index():
return render_template("index.html")
@app.get("/user/<name>")
def user(name: str):
return render_template("user.html", name=name)
render_template(name, **context):
- 第一个参数:模板文件名(相对于
templates/) - 后续
key=value:模板里能直接使用的变量
访问 http://127.0.0.1:5000/user/Richie 会得到:
<h1>Hello, Richie!</h1>
4. 模板语法速查
{# 注释 #}
{{ var }} {# 变量插值,自动转义 #}
{{ var | upper }} {# 过滤器,链式:{{ x | upper | trim }} #}
{{ "hello %s" | format(name) }} {# 调用 #}
{% if user %} {# 条件 #}
Hi, {{ user }}
{% elif other %}
Hi, guest
{% endif %}
{% for item in items %} {# 循环 #}
<li>{{ loop.index }} - {{ item }}</li>
{% else %} {# 列表为空时执行 #}
<li>no items</li>
{% endfor %}
{# Python 风格的字面量 #}
{{ [1, 2, 3] | length }} {# 3 #}
{{ {"a": 1, "b": 2} | tojson }} {# 用于把字典渲染成 JSON 字符串 #}
常用过滤器:upper / lower / trim / length / default(value, default_value) /
safe(关闭转义,慎用) / tojson / urlencode / replace / join。
5. 控制流示例
5.1 条件渲染
<!-- templates/profile.html -->
{% if user %}
<h1>欢迎回来,{{ user.name }}!</h1>
{% if user.is_admin %}
<p><a href="/admin">进入管理后台</a></p>
{% endif %}
{% else %}
<h1>请先 <a href="/login">登录</a></h1>
{% endif %}
5.2 列表渲染
<!-- templates/posts.html -->
<ul>
{% for post in posts %}
<li>
<a href="{{ url_for('show_post', post_id=post.id) }}">
{{ post.title }}
</a>
<small>— {{ post.author }} · {{ post.created_at | format_date }}</small>
</li>
{% else %}
<li>暂无文章</li>
{% endfor %}
</ul>
特殊循环变量 loop:
| 属性 | 说明 |
|---|---|
loop.index |
当前迭代(从 1 开始) |
loop.index0 |
当前迭代(从 0 开始) |
loop.first |
是否是第一个 |
loop.last |
是否是最后一个 |
loop.length |
序列总长度 |
6. 模板继承:避免重复 HTML
网站通常有统一的导航/页脚/样式骨架。Jinja2 的继承机制可以让我们写一次基模板,
子模板只覆盖其中“会变”的部分。
6.1 基模板 templates/base.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{% block title %}我的站点{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<a href="{{ url_for('index') }}">首页</a>
<nav>{% block nav %}{% endblock %}</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
© {{ now().year if now is defined else '2026' }} Richie
</footer>
</body>
</html>
6.2 子模板 templates/index.html
{% extends "base.html" %}
{% block title %}首页 - 我的站点{% endblock %}
{% block content %}
<h1>欢迎</h1>
<p>这是首页内容。</p>
{% endblock %}
extends必须是子模板的第一行。子模板可以选择性覆盖任意{% block %},
未覆盖的 block 保留父模板内容。
6.3 包含片段:include
复用度更高的“组件化”片段,可以用 include 引入:
{# templates/_card.html #}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ body }}</p>
</div>
{# 在其它模板中使用 #}
{% include "_card.html" %}
约定:可被复用的局部模板以下划线 _ 开头命名,便于区分。
7. 链接与静态资源:url_for
url_for(endpoint, **values) 通过**端点(endpoint)**生成 URL,避免硬编码:
<a href="{{ url_for('show_post', post_id=42) }}">查看文章</a>
{# 等价于 /posts/42,前提是路由定义为 @app.get('/posts/<int:post_id>') #}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="{{ url_for('static', filename='app.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="logo">
永远不要在模板里硬编码
/static/...,改静态目录名会牵一发动全身。
8. 把数据传给模板
8.1 通过关键字参数
@app.get("/hello/<name>")
def hello(name: str):
return render_template(
"hello.html",
name=name, # 单个变量
items=[1, 2, 3], # 列表
user={"id": 1, "name": name}, # 字典
)
8.2 通过 ContextProcessor 注入“全局变量”
如果某些变量在每个模板都要用(比如站点名、当前用户),可以自动注入:
@app.context_processor
def inject_globals():
return {
"site_name": "Richie 的博客",
"current_year": 2026,
}
之后任何模板里都能直接使用 {{ site_name }},无需 render_template 传参。
9. 完整可运行示例
# app.py
from datetime import datetime
from flask import Flask, render_template
app = Flask(__name__)
@app.context_processor
def inject_now():
return {"now": datetime.now}
@app.get("/")
def index():
return render_template("index.html")
@app.get("/user/<name>")
def user(name: str):
items = ["Python", "Flask", "Jinja2"]
return render_template("user.html", name=name, items=items)
{# templates/user.html #}
{% extends "base.html" %}
{% block title %}{{ name }} - 个人页{% endblock %}
{% block content %}
<h1>Hi, {{ name }}!</h1>
<p>你关注的标签:</p>
<ul>
{% for tag in items %}
<li>{{ loop.index }}. {{ tag }}</li>
{% endfor %}
</ul>
{% endblock %}
flask --app app run --debug
# 访问 http://127.0.0.1:5000/user/Richie
10. 常见问题
- 改了模板没生效? Flask 默认开启模板自动重载(
TEMPLATES_AUTO_RELOAD)。
生产环境会缓存,可在app.jinja_env.auto_reload = True强制刷新。 - 报错
TemplateNotFound? 检查文件是否在templates/目录、文件名后缀
是否正确(必须是.html/.j2等 Jinja2 识别的扩展名)。 - HTML 被转义了? 正常行为。如确需输出可信 HTML,用
{{ value | safe }},
但永远不要对用户输入使用safe。 - 想用 Vue / React? 在前后端分离的架构下,Flask 通常只返回 JSON
(jsonify),不再使用 Jinja2 模板。
小结
| 主题 | 现代实践 |
|---|---|
| 模板位置 | templates/ |
| 模板继承 | {% extends "base.html" %} + {% block %} |
| 组件复用 | {% include "_partial.html" %} |
| URL 生成 | url_for('endpoint', **values),不要硬编码 |
| 静态资源 | url_for('static', filename=...) |
| 全局数据 | @app.context_processor |
| 安全 | 默认开启 HTML 转义,不要滥用 safe |
下一步:通过 IP 查询案例 把请求-响应循环、模板、表单、JSON
四个能力串起来。