R / Richie全部文章 ↑

Python · 5 分钟阅读

3. Flask 请求-响应循环

目录


视图函数的核心职责是生成响应。但当响应是一段复杂的 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>
    &copy; {{ 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
四个能力串起来。