Flask 实战教程:面向 Python 开发者的轻量 Web 后端入门
Flask 的魅力在于“小”:它不会一开始就替你规定项目结构、数据库 ORM、表单库、登录系统和管理后台。但这种自由也意味着你需要自己决定边界。本文会从一个可运行的小项目开始,逐步把自由组织成可维护的工程结构。
如果你已经会 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 中大致会经过这些阶段:
- WSGI Server 接收 HTTP 请求;
- Flask 创建请求上下文;
- 根据 URL Rule 匹配视图函数;
- 执行
before_request 等钩子;
- 运行视图函数;
- 返回字符串、字典、模板响应、重定向或 Response 对象;
- 执行
after_request 等钩子;
- 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:
Flask 官方文档当前稳定版仍然以 pip install Flask 作为基础安装方式。生产项目建议再生成固定版本依赖:
1
|
pip freeze > requirements.txt
|
如果你准备写测试,顺手装上 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)
|
运行开发服务器:
浏览器打开:
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 规则匹配到视图函数,视图函数返回响应。
常见路由写法:
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、模板输出和数据库查询这些地方。
先看一个简单表单页面:
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() 函数来创建应用。
推荐结构:
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 |
初学时不要一次性把所有扩展装满。建议顺序是:
- 先用内存字典理解路由和请求;
- 再接 SQLite 和 SQLAlchemy;
- 再加数据库迁移;
- 页面项目加 Flask-WTF;
- 有登录需求再加 Flask-Login;
- 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
|
运行:
测试重点不是“把每一行都跑一遍”,而是保护接口行为:状态码、响应结构、错误分支、鉴权和关键业务规则。这样后面把内存字典替换成数据库时,测试能告诉你外部行为有没有变。
十、错误处理与状态码
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 或托管平台。
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 往往更省心。
十四、学习路线
建议按这个顺序继续:
- Jinja2 模板继承、宏和过滤器;
- Blueprint 和应用工厂;
- Flask-SQLAlchemy + Flask-Migrate;
- Flask-WTF 和 CSRF;
- Flask-Login;
- 错误处理和日志;
- pytest + test client;
- Gunicorn / Waitress / Docker 部署;
- Nginx、HTTPS 和线上监控。
Flask 的好处是你可以每次只学一小块。先把请求响应、模板和测试跑通,再慢慢引入数据库、登录、表单和部署能力,学习曲线会平滑很多。
参考资料