Skip to content

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. 安装 ​

bash
# 建虚拟环境
python -m venv .venv

# 激活(三选一)
.venv\Scripts\activate        # Windows PowerShell / CMD
source .venv/bin/activate     # macOS / Linux

# 安装
pip install Flask

用 uv 更快:

bash
uv init flask-demo && cd flask-demo
uv add flask

验证:

bash
python -c "import flask; print(flask.__version__)"
# 3.1.3

2. 第一个应用 ​

新建 app.py:

python
from flask import Flask

app = Flask(__name__)


@app.route("/")
def hello():
    return "Hello, World!"

启动:

bash
flask --app app run
#  * Running on http://127.0.0.1:5000

flask run 会自动找到 app.py 或 wsgi.py 里的 app 对象。开发时加 --debug 开启热重载和调试页:

bash
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,其他方法要显式声明:

python
@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 路径参数与转换器 ​

尖括号里的变量会作为关键字参数传给视图函数,转换器决定类型和匹配规则:

python
@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含 / 的字符串做文件路径、多级资源时用
uuidUUID 字符串

💡 转换器不匹配时 Flask 返回 404 而不是 400——因为它认为这条 URL 压根不存在。这点和 FastAPI 的 422 不同,写前端时要注意。

3.3 url_for:反向生成 URL ​

不要硬编码路径,用函数名反查:

python
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"

静态文件也用它:

python
url_for("static", filename="style.css")   # -> "/static/style.css"

好处:改了路由规则,所有引用自动跟着变。


4. 请求数据:request 对象 ​

request 是一个请求上下文代理,在视图函数内直接导入使用即可,不需要当参数传:

python
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
JSONrequest.get_json()需 Content-Type: application/json
上传文件request.files["avatar"]见下方示例
请求头request.headers.get("Authorization")
Cookierequest.cookies.get("session_id")
原始数据request.data / request.get_data()

4.1 JSON 请求体的正确姿势 ​

python
@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 上传文件 ​

python
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 允许视图函数返回多种类型,会自动转换:

python
# 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

其他常用响应函数:

python
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/。

python
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:

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:

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>
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. 配置 ​

python
# 方式一:直接改
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 做签名防止篡改:

python
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")}

两个必须知道的事

  1. 没有 SECRET_KEY 时访问 session 会直接抛 RuntimeError(Flask 2.0 起不再是警告)。
  2. Cookie 里的内容只是签名、没有加密,客户端能解开看到明文。绝对不要往 session 里放密码、token 等敏感数据。

生产环境的 SECRET_KEY 必须来自环境变量,并且每个环境不同:

python
import os

app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]

9. 错误处理 ​

python
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 错误页:

python
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. 请求钩子 ​

python
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):
    # 无论成功失败都会执行,用来释放资源(关连接、清临时文件)
    pass

g 是请求级的临时存储,一次请求内共享,请求结束自动销毁。

执行顺序:

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"]
返回 JSONreturn {"k": "v"}
带状态码return data, 201
带响应头return data, 201, {"X-Id": "1"}
渲染模板render_template("a.html", **ctx)
跳转redirect(url_for("endpoint"))
中断abort(404)
生成 URLurl_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_KEYapp.config["SECRET_KEY"] = ...
拿不到 JSON 报 415客户端没发 Content-Type: application/json用 get_json(silent=True) 自己兜底
request.args.get("page") 拿到字符串查询参数永远是字符串显式 int(request.args.get("page", 1))
路径转换器不匹配返回 404 而非 400Flask 认为 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=developmentFlask 2.3 已移除改用 --debug 或 app.debug = True
中文被转义成 \uXXXX默认 ensure_ascii=Trueapp.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 工程化:应用工厂与蓝图。

📖本文阅读--次|📊全站访问--次|👥访客--人