全部文章 笔记与思考

Flask 实战教程:面向 Python 开发者的轻量 Web 后端入门

面向已有 Python 基础的开发者,从第一个 Flask 路由开始掌握请求响应、模板、表单、Session、Blueprint、测试和生产部署。

Flask 实战教程:面向 Python 开发者的轻量 Web 后端入门

Flask 的魅力在于“小”:它不会一开始就替你规定项目结构、数据库 ORM、表单库、登录系统和管理后台。但这种自由也意味着你需要自己决定边界。本文会从一个可运行的小项目开始,逐步把自由组织成可维护的工程结构。

Flask 实战教程封面

如果你已经会 Python 函数、装饰器、虚拟环境和基础 HTTP,Flask 很适合作为第一个 Web 后端框架。它既能写返回 HTML 的传统网站,也能写 JSON API;既能单文件快速验证想法,也能用应用工厂、Blueprint 和扩展支撑中型项目。

你已有的 Python 能力 在 Flask 中如何对应
函数 一个视图函数处理一个路由
装饰器 @app.get()、@app.post() 绑定 URL
字典和列表 可以直接返回 JSON 风格数据
模板字符串思维 用 Jinja2 渲染 HTML 页面
模块拆分 用 Blueprint 组织路由
单元测试 用 Flask test client 测接口行为

一、Flask 是什么

Flask 是一个轻量级 WSGI Web 框架。它的核心只处理 Web 框架最基础的几件事:

  • URL 路由;
  • 请求对象和响应对象;
  • Cookie、Session、上下文;
  • Jinja2 模板集成;
  • 静态文件;
  • 错误处理;
  • 扩展机制。

数据库、表单校验、登录认证、后台任务、API 文档等能力通常通过扩展或你的业务代码组合出来。这和 Django 的“全家桶”风格不同:Django 默认给你很多约定,Flask 默认给你一个很薄但很灵活的核心。

Flask 请求生命周期

一次请求在 Flask 中大致会经过这些阶段:

  1. WSGI Server 接收 HTTP 请求;
  2. Flask 创建请求上下文;
  3. 根据 URL Rule 匹配视图函数;
  4. 执行 before_request 等钩子;
  5. 运行视图函数;
  6. 返回字符串、字典、模板响应、重定向或 Response 对象;
  7. 执行 after_request 等钩子;
  8. WSGI Server 把响应发回客户端。

你可以把 Flask 理解成一个“请求响应协调器”:它不会强迫你怎么写业务,但会把请求到函数、函数到响应这条主线打通。


二、环境准备

先创建虚拟环境:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mkdir flask-shop
cd flask-shop

python -m venv .venv

# Windows PowerShell
.\.venv\Scripts\Activate.ps1

# macOS / Linux
source .venv/bin/activate

安装 Flask:

1
pip install Flask

Flask 官方文档当前稳定版仍然以 pip install Flask 作为基础安装方式。生产项目建议再生成固定版本依赖:

1
pip freeze > requirements.txt

如果你准备写测试,顺手装上 pytest:

1
pip install pytest

三、第一个 Flask 应用

创建 app.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from flask import Flask, jsonify, request


app = Flask(__name__)

items: dict[int, dict[str, object]] = {}
next_id = 1


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/items")
def list_items():
    q = request.args.get("q", "").strip().lower()
    result = list(items.values())

    if q:
        result = [item for item in result if q in str(item["name"]).lower()]

    return jsonify(result)


@app.post("/items")
def create_item():
    global next_id

    data = request.get_json(silent=True) or {}
    name = str(data.get("name", "")).strip()
    price = data.get("price")

    if not name:
        return {"error": "name is required"}, 400

    try:
        price = float(price)
    except (TypeError, ValueError):
        return {"error": "price must be a number"}, 400

    if price <= 0:
        return {"error": "price must be greater than 0"}, 400

    item = {"id": next_id, "name": name, "price": price}
    items[next_id] = item
    next_id += 1

    return jsonify(item), 201


@app.get("/items/<int:item_id>")
def read_item(item_id: int):
    item = items.get(item_id)

    if item is None:
        return {"error": "item not found"}, 404

    return jsonify(item)

运行开发服务器:

1
flask --app app run

浏览器打开:

  • http://127.0.0.1:5000/health
  • http://127.0.0.1:5000/items

用 curl 创建一个商品:

1
2
3
curl -X POST http://127.0.0.1:5000/items \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"Keyboard\",\"price\":299}"

开发时如果想开启调试模式,可以加 --debug:

1
flask --app app run --debug

调试模式会自动重载代码并展示调试信息,但不要在生产环境开启。


四、路由、请求与响应

Flask 的基本工作方式很直接:URL 规则匹配到视图函数,视图函数返回响应。

Flask 路由、视图函数与模板

常见路由写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@app.get("/")
def index():
    return "Hello Flask"


@app.get("/users/<int:user_id>")
def read_user(user_id: int):
    return {"user_id": user_id}


@app.post("/users")
def create_user():
    data = request.get_json()
    return {"received": data}, 201

请求数据通常来自几个位置:

来源 写法 场景
查询参数 request.args.get("q") /items?q=book
路径参数 /items/<int:item_id> 资源 ID
JSON body request.get_json() API 请求体
表单 request.form["name"] HTML 表单提交
Header request.headers.get("X-Token") 鉴权、追踪 ID
Cookie request.cookies.get("theme") 轻量偏好

视图函数可以返回多种值:

1
2
3
4
5
6
return "Hello"
return {"status": "ok"}
return jsonify(items)
return jsonify(item), 201
return redirect("/items")
return render_template("items.html", items=items)

Flask 适合初学者的一个原因是:你可以先用简单返回值跑通,等项目复杂后再引入更严格的响应结构和错误处理。


五、模板:用 Jinja2 返回 HTML

Flask 不只适合写 API,它也很适合写服务端渲染页面。创建目录:

1
2
3
4
5
flask-shop/
├── app.py
└── templates/
    ├── base.html
    └── items.html

templates/base.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <title>{% block title %}Flask Shop{% endblock %}</title>
</head>
<body>
    <header>
        <h1>Flask Shop</h1>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>

templates/items.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{% extends "base.html" %}

{% block title %}商品列表{% endblock %}

{% block content %}
    <h2>商品列表</h2>

    <ul>
        {% for item in items %}
            <li>{{ item.name }} - {{ item.price }}</li>
        {% else %}
            <li>暂无商品</li>
        {% endfor %}
    </ul>
{% endblock %}

在 app.py 中增加页面路由:

1
2
3
4
5
6
from flask import render_template


@app.get("/")
def index():
    return render_template("items.html", items=list(items.values()))

Jinja2 默认会对变量进行 HTML 转义,所以 {{ item.name }} 比自己拼接 HTML 字符串安全得多。除非你非常明确知道数据来源可信,否则不要随便使用 |safe。


六、表单、Session 与安全边界

Flask 给你自由,但安全边界需要主动设计。尤其是表单、Session、Cookie、模板输出和数据库查询这些地方。

Flask 表单、Session 与安全边界

先看一个简单表单页面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<form method="post" action="/items/form">
    <label>
        名称
        <input name="name" required>
    </label>

    <label>
        价格
        <input name="price" type="number" step="0.01" min="0.01" required>
    </label>

    <button type="submit">创建</button>
</form>

后端处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import redirect, request, url_for


@app.post("/items/form")
def create_item_from_form():
    global next_id

    name = request.form.get("name", "").strip()
    price_raw = request.form.get("price")

    if not name:
        return "name is required", 400

    try:
        price = float(price_raw)
    except (TypeError, ValueError):
        return "price must be a number", 400

    item = {"id": next_id, "name": name, "price": price}
    items[next_id] = item
    next_id += 1

    return redirect(url_for("index"))

上面只是演示最小流程。真实项目建议使用 Flask-WTF 或其他表单库来处理字段校验和 CSRF 防护。

Session 需要设置密钥:

1
2
3
4
import os

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-only-change-me")

然后可以使用 session:

1
2
3
4
5
6
7
from flask import session


@app.post("/login")
def login():
    session["user_id"] = 1
    return {"status": "logged in"}

需要注意:Flask 默认 Session 是签名 Cookie。客户端不能随意篡改它,但可以看到它的内容,所以不要把密码、Token、隐私数据或大量数据放进 Session。


七、项目变大后:应用工厂与 Blueprint

单文件适合入门,但项目继续长大后,app.py 很容易变成一个难维护的大文件。Flask 官方文档推荐使用应用工厂模式,也就是写一个 create_app() 函数来创建应用。

Flask 应用工厂与 Blueprint

推荐结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
flask-shop/
├── app/
│   ├── __init__.py
│   ├── blueprints/
│   │   ├── items.py
│   │   └── users.py
│   ├── templates/
│   │   ├── base.html
│   │   └── items/
│   │       └── list.html
│   ├── static/
│   │   └── style.css
│   └── config.py
├── tests/
│   └── test_items.py
└── requirements.txt

app/__init__.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask


def create_app(test_config: dict | None = None) -> Flask:
    app = Flask(__name__, instance_relative_config=True)

    app.config.from_mapping(
        SECRET_KEY="dev",
    )

    if test_config is not None:
        app.config.update(test_config)

    from app.blueprints.items import bp as items_bp

    app.register_blueprint(items_bp)

    @app.get("/health")
    def health() -> dict[str, str]:
        return {"status": "ok"}

    return app

app/blueprints/items.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from flask import Blueprint, jsonify, request


bp = Blueprint("items", __name__, url_prefix="/items")

items: dict[int, dict[str, object]] = {}
next_id = 1


@bp.get("")
def list_items():
    return jsonify(list(items.values()))


@bp.post("")
def create_item():
    global next_id

    data = request.get_json(silent=True) or {}
    name = str(data.get("name", "")).strip()
    price = float(data.get("price", 0))

    if not name or price <= 0:
        return {"error": "invalid item"}, 400

    item = {"id": next_id, "name": name, "price": price}
    items[next_id] = item
    next_id += 1

    return jsonify(item), 201

运行应用工厂:

1
flask --app app run --debug

这里的 --app app 会找到 app 包中的 create_app()。这种结构的优点是:测试时可以创建独立配置的应用,扩展初始化也更清晰。


八、数据库与扩展怎么选

Flask 不自带 ORM。常见组合是:

需求 常用选择
ORM Flask-SQLAlchemy 或 SQLAlchemy
数据库迁移 Flask-Migrate / Alembic
登录态 Flask-Login
表单和 CSRF Flask-WTF
API 参数校验 Marshmallow、Pydantic 或 Webargs
REST API 快速组织 Flask-RESTful、Flask-Smorest
后台任务 Celery、RQ、Dramatiq

初学时不要一次性把所有扩展装满。建议顺序是:

  1. 先用内存字典理解路由和请求;
  2. 再接 SQLite 和 SQLAlchemy;
  3. 再加数据库迁移;
  4. 页面项目加 Flask-WTF;
  5. 有登录需求再加 Flask-Login;
  6. API 项目再考虑 Marshmallow、Pydantic 或 OpenAPI 相关扩展。

Flask 的扩展生态很丰富,但扩展越多,越需要清晰的初始化位置。应用工厂模式正是为了解决这个问题。


九、测试:用 test client 固定接口行为

Flask 自带 test client,适合直接测试接口。

tests/test_items.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from app import create_app


def test_health():
    app = create_app({"TESTING": True})
    client = app.test_client()

    response = client.get("/health")

    assert response.status_code == 200
    assert response.get_json() == {"status": "ok"}


def test_create_item():
    app = create_app({"TESTING": True})
    client = app.test_client()

    response = client.post(
        "/items",
        json={"name": "Mouse", "price": 99},
    )

    assert response.status_code == 201
    body = response.get_json()
    assert body["name"] == "Mouse"
    assert body["price"] == 99


def test_reject_invalid_item():
    app = create_app({"TESTING": True})
    client = app.test_client()

    response = client.post("/items", json={"name": "", "price": -1})

    assert response.status_code == 400

运行:

1
pytest

测试重点不是“把每一行都跑一遍”,而是保护接口行为:状态码、响应结构、错误分支、鉴权和关键业务规则。这样后面把内存字典替换成数据库时,测试能告诉你外部行为有没有变。


十、错误处理与状态码

Flask 允许你直接返回 (body, status_code):

1
return {"error": "item not found"}, 404

也可以使用 abort:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from flask import abort


@app.get("/items/<int:item_id>")
def read_item(item_id: int):
    item = items.get(item_id)

    if item is None:
        abort(404)

    return item

如果想统一错误结构,可以注册错误处理器:

1
2
3
4
5
6
7
8
@app.errorhandler(404)
def not_found(error):
    return {"error": "not found"}, 404


@app.errorhandler(500)
def internal_error(error):
    return {"error": "internal server error"}, 500

常用状态码:

状态码 场景
200 OK 查询成功
201 Created 创建成功
204 No Content 删除成功且无响应体
400 Bad Request 参数语义错误
401 Unauthorized 未登录或凭证无效
403 Forbidden 已登录但无权限
404 Not Found 资源不存在
409 Conflict 资源冲突
500 Internal Server Error 未预期服务端错误

如果你写的是 API,建议从一开始就统一错误格式。否则前端会在不同接口里解析不同形状的错误响应。


十一、从开发到生产部署

本地开发命令:

1
flask --app app run --debug

但 Flask 官方文档明确提醒:内置开发服务器只用于本地开发,不要用于生产。生产环境应交给 WSGI Server 或托管平台。

Flask 从开发到生产部署

Windows 上可以用 Waitress:

1
2
pip install waitress
waitress-serve --host=0.0.0.0 --port=8000 "app:create_app()"

Linux 服务器上常见组合是 Gunicorn + Nginx:

1
2
pip install gunicorn
gunicorn "app:create_app()" --bind 0.0.0.0:8000 --workers 4

一个最小 Dockerfile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

EXPOSE 8000
CMD ["gunicorn", "app:create_app()", "--bind", "0.0.0.0:8000", "--workers", "4"]

生产前至少补齐:

项目 原因
固定依赖版本 避免构建时引入不可预期更新
环境变量 密钥、数据库地址、第三方 Token 不写进代码
SECRET_KEY Session、CSRF 等安全能力依赖它
数据库迁移 schema 变化可追踪、可回滚
结构化日志 方便排查线上问题
健康检查 让负载均衡知道应用是否可用
HTTPS 保护 Cookie、登录态和敏感请求
反向代理 处理静态资源、TLS、压缩、限流等入口能力

十二、Flask 常见坑

坑 更稳的做法
所有代码堆在 app.py 入门可以,项目变大后用应用工厂和 Blueprint
把开发服务器用于生产 生产使用 Gunicorn、Waitress、uWSGI 或托管平台
不设置 SECRET_KEY 从环境变量读取,并在生产环境使用强随机值
手写 HTML 字符串 用 Jinja2 模板,避免未转义内容混入页面
表单写入不做 CSRF 页面项目使用 Flask-WTF 或等价方案
SQL 用字符串拼接 使用 ORM 或参数化 SQL
错误响应格式不统一 API 项目尽早统一错误结构
Blueprint 里塞太多业务 路由负责 HTTP,复杂规则移到 service 层

十三、Flask 与 FastAPI 怎么选

维度 Flask FastAPI
框架风格 轻量、自由、扩展组合 类型驱动、API 契约强
页面渲染 Jinja2 体验成熟 也能做,但主流认知偏 API
请求校验 需要自己选扩展或手写 Pydantic 深度集成
自动文档 需要扩展 内置 OpenAPI 文档
异步支持 不是核心优势 ASGI 原生更适合异步 API
入门感受 概念少,适合理解 Web 本质 工程化 API 体验更完整

如果你想理解 Web 后端的底层主线,或者写一个页面 + 少量 API 的项目,Flask 很舒服。如果你目标是类型契约明确的纯 API 服务,尤其需要自动 OpenAPI 文档,FastAPI 往往更省心。


十四、学习路线

建议按这个顺序继续:

  1. Jinja2 模板继承、宏和过滤器;
  2. Blueprint 和应用工厂;
  3. Flask-SQLAlchemy + Flask-Migrate;
  4. Flask-WTF 和 CSRF;
  5. Flask-Login;
  6. 错误处理和日志;
  7. pytest + test client;
  8. Gunicorn / Waitress / Docker 部署;
  9. Nginx、HTTPS 和线上监控。

Flask 的好处是你可以每次只学一小块。先把请求响应、模板和测试跑通,再慢慢引入数据库、登录、表单和部署能力,学习曲线会平滑很多。


参考资料