Flask 快速上手:从零跑起第一个 Web 应用
Flask 是一个基于 WSGI 的轻量级 Python Web 框架,构建在 Werkzeug(WSGI 工具库)和 Jinja2(模板引擎)之上。 它的哲学是「给你最小内核,其余自己选」——所以它上手极快,但也意味着 ORM、迁移、认证这些都要自己搭配。
这篇是使用教程,不讲框架原理,只讲日常开发要用的东西。想先搞清 Flask 和 FastAPI 怎么选,可以看 FastAPI 与 Flask 深度对比。
版本说明
本文基于 Flask 3.1.x(要求 Python ≥ 3.9)。网上大量教程停留在 Flask 1.x/2.x,FLASK_ENV、before_first_request 这些写法已经被移除,照抄会报错——文末「常见坑」列了这类坑。
1. 安装
# 建虚拟环境
python -m venv .venv
# 激活(三选一)
.venv\Scripts\activate # Windows PowerShell / CMD
source .venv/bin/activate # macOS / Linux
# 安装
pip install Flask用 uv 更快:
uv init flask-demo && cd flask-demo
uv add flask验证:
python -c "import flask; print(flask.__version__)"
# 3.1.32. 第一个应用
新建 app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, World!"启动:
flask --app app run
# * Running on http://127.0.0.1:5000flask run 会自动找到 app.py 或 wsgi.py 里的 app 对象。开发时加 --debug 开启热重载和调试页:
flask --app app run --debug
# 或
flask --app app run --reload --debug别用开发服务器上生产
flask run 用的是 Werkzeug 自带的开发服务器,单进程、性能差、不适合生产。生产环境用 gunicorn(Linux)或 waitress(Windows),详见工程化那篇。
2.1 一个请求是怎么被处理的
记住这条链路,后面所有的钩子、错误处理、中间件就都有位置可放了。
3. 路由
3.1 基本写法
@app.route() 默认只接受 GET,其他方法要显式声明:
@app.route("/items", methods=["GET", "POST"])
def items():
...
# Flask 2.0 起提供了更直观的快捷装饰器
@app.get("/items")
def list_items():
...
@app.post("/items")
def create_item():
...常用 HTTP 方法快捷装饰器:@app.get / @app.post / @app.put / @app.patch / @app.delete。
3.2 路径参数与转换器
尖括号里的变量会作为关键字参数传给视图函数,转换器决定类型和匹配规则:
@app.get("/users/<int:user_id>")
def get_user(user_id: int): # user_id 已是 int
return {"user_id": user_id}
@app.get("/files/<path:filepath>") # path 允许包含斜杠
def get_file(filepath: str):
return {"path": filepath}| 转换器 | 匹配内容 | 说明 |
|---|---|---|
string | 不含 / 的字符串 | 默认 |
int | 整数 | 匹配失败直接 404,不会进视图函数 |
float | 浮点数 | |
path | 含 / 的字符串 | 做文件路径、多级资源时用 |
uuid | UUID 字符串 |
💡 转换器不匹配时 Flask 返回 404 而不是 400——因为它认为这条 URL 压根不存在。这点和 FastAPI 的 422 不同,写前端时要注意。
3.3 url_for:反向生成 URL
不要硬编码路径,用函数名反查:
from flask import url_for
@app.get("/users/<int:user_id>")
def get_user(user_id: int):
return {"url": url_for("get_user", user_id=42)}
# -> "/users/42"静态文件也用它:
url_for("static", filename="style.css") # -> "/static/style.css"好处:改了路由规则,所有引用自动跟着变。
4. 请求数据:request 对象
request 是一个请求上下文代理,在视图函数内直接导入使用即可,不需要当参数传:
from flask import request| 数据来源 | 取值方式 | 备注 |
|---|---|---|
| 查询参数 | request.args.get("page", "1") | ?page=2&tag=a&tag=b,多个同名用 getlist("tag") |
| 表单 | request.form["username"] | application/x-www-form-urlencoded 或 multipart |
| JSON | request.get_json() | 需 Content-Type: application/json |
| 上传文件 | request.files["avatar"] | 见下方示例 |
| 请求头 | request.headers.get("Authorization") | |
| Cookie | request.cookies.get("session_id") | |
| 原始数据 | request.data / request.get_data() |
4.1 JSON 请求体的正确姿势
@app.post("/items")
def create_item():
# 客户端没带 application/json 时 get_json() 会抛 415
data = request.get_json(silent=True)
# silent=True 让它返回 None,自己给一个友好的 400
if data is None:
return {"error": "请求体必须是合法 JSON"}, 400
name = data.get("name")
if not isinstance(name, str) or not name:
return {"error": "name 必填"}, 400
return {"name": name}, 201没有自动校验
Flask 不会帮你校验字段类型和约束,这些 if 都得自己写。想要 FastAPI 那种「声明 Pydantic 模型就自动校验 + 自动生成文档」的效果,需要额外引入 flask-smorest、marshmallow 等扩展。
4.2 上传文件
import os
from uuid import uuid4
from flask import request
from werkzeug.utils import secure_filename
UPLOAD_DIR = "uploads"
ALLOWED = {".png", ".jpg", ".jpeg", ".webp"}
@app.post("/upload")
def upload():
file = request.files.get("avatar")
if file is None or file.filename == "":
return {"error": "未选择文件"}, 400
# secure_filename 会剥离路径分隔符,防目录穿越
safe = secure_filename(file.filename)
ext = os.path.splitext(safe)[1].lower()
if ext not in ALLOWED:
return {"error": f"仅支持 {', '.join(ALLOWED)}"}, 400
# 用 uuid 重命名,避免同名覆盖和猜解
filename = f"{uuid4().hex}{ext}"
file.save(os.path.join(UPLOAD_DIR, filename))
return {"filename": filename}, 201⚠️
secure_filename会处理掉../,但它不能替代白名单校验。扩展名白名单 + 服务端生成文件名,这两条都要做。
5. 响应:四种写法
Flask 允许视图函数返回多种类型,会自动转换:
# 1) 字符串 —— text/html
@app.get("/text")
def text():
return "纯文本"
# 2) 字典 / 列表 —— 自动转 JSON(Flask 1.1+)
@app.get("/json")
def json_view():
return {"message": "自动 JSON 化", "code": 0}
# 3) 元组 —— (响应体, 状态码, 响应头)
@app.post("/items")
def create():
return {"id": 1}, 201, {"X-Request-Id": "abc123"}
# 4) Response 对象 —— 需要精细控制时
from flask import make_response
@app.get("/custom")
def custom():
resp = make_response({"ok": True})
resp.status_code = 202
resp.headers["X-Custom"] = "yes"
resp.set_cookie("theme", "dark", max_age=3600, httponly=True)
return resp其他常用响应函数:
from flask import jsonify, redirect, url_for, abort, send_from_directory
return jsonify(items) # 显式 JSON
return redirect(url_for("get_user", user_id=1)) # 302 跳转
abort(404, description="用户不存在") # 立即中断并触发错误处理器
return send_from_directory("uploads", "a.png") # 安全地发文件dict 和 jsonify 的区别
返回 dict 时 Flask 用的是应用级 JSON 配置(app.json);jsonify() 完全等价,只是更显式。想调整中文不被转义、键排序等行为,配置 app.json.ensure_ascii = False、app.json.sort_keys = False。
6. Jinja2 模板
做服务端渲染的页面时用模板。默认目录是 templates/。
from flask import render_template
@app.get("/hello/<name>")
def hello(name: str):
return render_template("hello.html", name=name, items=["A", "B", "C"])templates/hello.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Hello {{ name }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<h1>你好,{{ name }}</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>6.1 模板继承
这是 Jinja2 最省事的地方——把公共部分抽成 base.html:
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{% block title %}我的站点{% endblock %}</title>
</head>
<body>
<nav>{% include "_nav.html" %}</nav>
<main>{% block content %}{% endblock %}</main>
</body>
</html><!-- templates/user.html -->
{% extends "base.html" %}
{% block title %}{{ user.name }} 的主页{% endblock %}
{% block content %}
<h1>{{ user.name }}</h1>
{% endblock %}安全提示:Jinja2 默认自动转义 HTML, 不会造成 XSS。但 |safe 过滤器和 {% autoescape false %} 会关掉保护,永远不要对用户输入用。
7. 静态文件
默认目录 static/,启动后通过 /static/<filename> 访问:
项目/
├── app.py
├── templates/
│ └── hello.html
└── static/
└── style.css在模板里始终用 url_for('static', filename='style.css') 生成路径,不要写死 /static/...(改 static_url_path 时会失效)。
8. 配置
# 方式一:直接改
app.config["SECRET_KEY"] = "dev-secret"
app.config["JSON_AS_ASCII"] = False
# 方式二:批量
app.config.from_mapping(
SECRET_KEY="dev-secret",
DATABASE_URL="sqlite:///app.db",
)
# 方式三:从类加载(推荐,可做环境分层)
class Config:
SECRET_KEY = "change-me"
DATABASE_URL = "sqlite:///app.db"
app.config.from_object(Config)
# 方式四:从环境变量读取(Flask 2.1+)
# 会把 FLASK_DATABASE_URL 读成 config["DATABASE_URL"]
app.config.from_prefixed_env()8.1 SECRET_KEY 是干什么的
Flask 的 session 把数据存在客户端 Cookie 里,用 SECRET_KEY 做签名防止篡改:
from flask import session
@app.post("/login")
def login():
session["user_id"] = 1 # 写进签名 Cookie
return {"ok": True}
@app.get("/me")
def me():
return {"user_id": session.get("user_id")}两个必须知道的事
- 没有
SECRET_KEY时访问session会直接抛RuntimeError(Flask 2.0 起不再是警告)。 - Cookie 里的内容只是签名、没有加密,客户端能解开看到明文。绝对不要往 session 里放密码、token 等敏感数据。
生产环境的 SECRET_KEY 必须来自环境变量,并且每个环境不同:
import os
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]9. 错误处理
from flask import abort
@app.get("/users/<int:user_id>")
def get_user(user_id: int):
user = find_user(user_id)
if user is None:
abort(404, description="用户不存在")
return user统一接管错误,返回 JSON 而不是默认的 HTML 错误页:
from werkzeug.exceptions import HTTPException
@app.errorhandler(404)
def not_found(e):
return {"error": "资源不存在", "code": 404}, 404
@app.errorhandler(HTTPException)
def handle_http_exception(e: HTTPException):
# 兜底所有 HTTP 异常,保证 API 永远返回 JSON
return {"error": e.description, "code": e.code}, e.code
@app.errorhandler(Exception)
def handle_unexpected(e: Exception):
app.logger.exception("未捕获异常")
# 生产环境不要回显堆栈
return {"error": "服务器内部错误", "code": 500}, 500只给 API 用这套
如果同一个应用既有页面又有 API,按路径前缀分别处理更清晰(比如 /api/ 走 JSON 处理器,其他走 HTML 处理器),可以读 request.path 判断。
10. 请求钩子
import time
from flask import request, g
@app.before_request
def start_timer():
g.start = time.perf_counter()
# 返回 None 表示继续处理;返回 Response 则直接短路返回
@app.after_request
def log_duration(response):
# 必须返回 response
cost = (time.perf_counter() - g.start) * 1000
response.headers["X-Response-Time"] = f"{cost:.1f}ms"
app.logger.info("%s %s -> %s %.1fms", request.method, request.path,
response.status_code, cost)
return response
@app.teardown_request
def cleanup(exc):
# 无论成功失败都会执行,用来释放资源(关连接、清临时文件)
passg 是请求级的临时存储,一次请求内共享,请求结束自动销毁。
执行顺序:
Flask 2.3 起 before_first_request 已移除
想做「应用启动时初始化一次」的事,改用应用工厂里的显式初始化,或 with app.app_context(): 块——见工程化那篇。
11. 常用写法速查
| 需求 | 写法 |
|---|---|
| 声明路由 | @app.get("/path") / @app.post("/path") |
| 路径参数 | @app.get("/u/<int:uid>") |
| 查询参数 | request.args.get("page", "1") |
| 查询参数多值 | request.args.getlist("tag") |
| JSON 请求体 | request.get_json(silent=True) |
| 表单 | request.form["name"] |
| 上传文件 | request.files["avatar"] |
| 返回 JSON | return {"k": "v"} |
| 带状态码 | return data, 201 |
| 带响应头 | return data, 201, {"X-Id": "1"} |
| 渲染模板 | render_template("a.html", **ctx) |
| 跳转 | redirect(url_for("endpoint")) |
| 中断 | abort(404) |
| 生成 URL | url_for("endpoint", arg=1) |
| 读配置 | app.config["KEY"] / current_app.config |
| 记日志 | app.logger.info(...) |
12. 常见坑
| 现象 | 原因 | 解决 |
|---|---|---|
启动报 Could not locate a Flask application | 没告诉 Flask 入口在哪 | flask --app app run,或设 FLASK_APP=app |
| 改了代码不生效 | 没开调试/重载 | flask --app app run --debug |
访问 session 报 RuntimeError: The session is unavailable | 没配 SECRET_KEY | app.config["SECRET_KEY"] = ... |
| 拿不到 JSON 报 415 | 客户端没发 Content-Type: application/json | 用 get_json(silent=True) 自己兜底 |
request.args.get("page") 拿到字符串 | 查询参数永远是字符串 | 显式 int(request.args.get("page", 1)) |
| 路径转换器不匹配返回 404 而非 400 | Flask 认为 URL 不存在 | 前端按 404 处理,或改用 string 再自己校验 |
两个路由函数同名导致 url_for 报错 | 端点名默认取函数名,必须全局唯一 | 加 endpoint= 参数区分 |
| 循环导入 | app.py 和 views.py 互相 import | 用应用工厂 + 蓝图,见下一篇 |
| 模板改了不生效 | 模板默认有缓存 | 开发时开 --debug,或 app.config["TEMPLATES_AUTO_RELOAD"] = True |
| 上传大文件报 413 | 超过 MAX_CONTENT_LENGTH | 调整 app.config["MAX_CONTENT_LENGTH"] |
照抄老教程用 FLASK_ENV=development | Flask 2.3 已移除 | 改用 --debug 或 app.debug = True |
中文被转义成 \uXXXX | 默认 ensure_ascii=True | app.json.ensure_ascii = False |
13. 小结
| 主题 | 关键点 |
|---|---|
| 定位 | WSGI 轻量框架,内核小、自由度高,一切按需搭配 |
| 路由 | @app.get/post/... + 尖括号转换器;url_for 反向生成 URL |
| 请求 | request 上下文代理:args / form / get_json() / files |
| 响应 | 字符串、dict、元组、Response 四种返回形式 |
| 模板 | Jinja2,extends + block 做继承,默认自动转义防 XSS |
| 配置 | from_object 分层 + from_prefixed_env 读环境变量;SECRET_KEY 必配 |
| 扩展点 | errorhandler 统一错误、before/after_request 做横切 |
| 边界 | 校验、文档、依赖注入都要自己搭——需要这些时考虑 FastAPI |
下一篇会把单文件拆成可维护的工程结构:Flask 工程化:应用工厂与蓝图。