Flask 是一个轻量、显式、可扩展的 Python Web 微框架:核心只做路由、请求/响应与 WSGI 粘合,模板、ORM、表单、认证等按需用扩展或自己写。本篇按官方文档的写法组织,尽量给出可直接粘贴改跑的片段;默认假设 Python 3.10+。
和 Django / FastAPI 比一句:Django 电池齐全(Admin、ORM、用户系统开箱即用),适合大而全的统一约定;FastAPI 以类型注解 + OpenAPI 见长,异步与 API 文档友好;Flask 最小内核、自由度高,原型、教学、中小业务与「自己拼积木」的场景最常见。选库看团队约定与需求形状,不是谁更「高级」。
1. 简介与适用
适合:
- 快速搭 CRUD、管理后台、Webhook、内部工具
- 想清楚「每个请求怎么进来、怎么出去」,而不是先吞一整套全家桶
- 用 Blueprint / 扩展逐步长大,而不是一开始就被脚手架绑死
不太适合或要慎重:
- 需要开箱 Admin + 强约定的大型单体 → Django 往往更省心
- 以 JSON API + 严格 schema / 异步为主 → FastAPI / Starlette 更对口
- 极致性能的纯 ASGI 流式服务 → 也要评估是否还用同步 WSGI 路径
对多数「先跑起来再演进」的项目,先把应用工厂、蓝图和配置分层想清楚,再谈扩展。
2. 环境:venv、pip、安装;应用工厂 vs 单文件
2.1 虚拟环境与安装
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -U pip
pip install flask
验证:
python -c "import flask; print(flask.__version__)"
建议把依赖钉死:
pip freeze > requirements.txt
# 之后:pip install -r requirements.txt
2.2 最小单文件应用(适合入门)
hello.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, Flask!"
if __name__ == "__main__":
app.run(debug=True)
开发时推荐用 CLI(比直接 python hello.py 更贴近文档):
export FLASK_APP=hello
flask run --debug
# 浏览器打开 http://127.0.0.1:5000/
2.3 应用工厂(推荐进阶 / 多配置)
单文件在测试、多环境、扩展初始化时容易缠在一起。官方推荐 application factory:用函数创建 app,配置与扩展在工厂里绑定。
myapp/
__init__.py # create_app()
config.py
views.py
templates/
static/
wsgi.py # 生产入口:from myapp import create_app; app = create_app()
myapp/__init__.py:
from flask import Flask
def create_app(config_object="myapp.config.DevConfig"):
app = Flask(__name__)
app.config.from_object(config_object)
from myapp import views
app.register_blueprint(views.bp)
return app
wsgi.py:
from myapp import create_app
app = create_app()
export FLASK_APP=wsgi:app
flask run --debug
何时用单文件:教程、脚本、极小工具。
何时用工厂:要测多套配置、挂多个 Blueprint、生产/开发切换。
3. 路由、变量规则、HTTP 方法、request / response / redirect / abort
3.1 基础路由与变量
from flask import Flask, abort
app = Flask(__name__)
@app.route("/")
def index():
return "home"
@app.route("/user/<username>")
def show_user(username):
return f"User: {username}"
@app.route("/post/<int:post_id>")
def show_post(post_id):
return f"Post #{post_id}"
@app.route("/path/<path:subpath>")
def show_subpath(subpath):
return f"Subpath: {subpath}"
常用转换器:string(默认)、int、float、path、uuid。
3.2 HTTP 方法
from flask import request
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
return "do login"
return "show login form"
只允许某些方法时,其它方法会自动 405。
3.3 request / 响应 / redirect / abort
from flask import request, jsonify, make_response, redirect, url_for, abort
@app.route("/api/echo", methods=["POST"])
def echo():
data = request.get_json(silent=True) or {}
# request.args → 查询串 ?q=1
# request.form → 表单字段
# request.files → 上传文件
# request.headers / request.cookies
return jsonify({"ok": True, "data": data})
@app.route("/old")
def old():
return redirect(url_for("index"), code=302)
@app.route("/secret")
def secret():
token = request.headers.get("X-Token")
if token != "s3cr3t":
abort(401) # 或 abort(403)、abort(404)
resp = make_response("ok", 200)
resp.headers["X-App"] = "demo"
return resp
url_for("endpoint_name", **params) 按端点名生成 URL,避免硬编码路径;Blueprint 里端点通常是 blueprint_name.view_func_name。
4. Jinja2 模板、静态文件、表单
4.1 模板与继承
默认模板目录:templates/。
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') }}">
</head>
<body>
<header><a href="{{ url_for('index') }}">首页</a></header>
<main>{% block content %}{% endblock %}</main>
</body>
</html>
templates/hello.html:
{% extends "base.html" %}
{% block title %}你好 · {{ name }}{% endblock %}
{% block content %}
<h1>你好,{{ name }}!</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% else %}
<li>暂无条目</li>
{% endfor %}
</ul>
{% endblock %}
视图:
from flask import render_template
@app.route("/hello/<name>")
def hello(name):
return render_template("hello.html", name=name, items=["a", "b"])
常用语法:{{ expr }}、{% if %} / {% for %}、过滤器 |e(默认转义)、{% include %}、宏 macro。
4.2 静态文件
默认 static/,URL 前缀 /static/...:
<img src="{{ url_for('static', filename='logo.png') }}" alt="logo">
4.3 表单:原生 HTML 或 WTForms
原生(教学最直观):
<form method="post" action="{{ url_for('subscribe') }}">
<input name="email" type="email" required>
<button type="submit">订阅</button>
</form>
from flask import request, redirect, url_for, flash, render_template
@app.route("/subscribe", methods=["GET", "POST"])
def subscribe():
if request.method == "POST":
email = (request.form.get("email") or "").strip()
if "@" not in email:
flash("邮箱格式不对", "error")
return redirect(url_for("subscribe"))
flash(f"已订阅:{email}", "ok")
return redirect(url_for("index"))
return render_template("subscribe.html")
记得设 app.secret_key,flash / session 才可用。
WTForms + Flask-WTF(校验与 CSRF 更省心):
pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Email
class SubscribeForm(FlaskForm):
email = StringField("邮箱", validators=[DataRequired(), Email()])
submit = SubmitField("订阅")
模板里用 form.hidden_tag() 输出 CSRF,字段用 {{ form.email() }}。生产务必配置可靠的 SECRET_KEY。
5. Blueprint:组织大项目
Blueprint 把路由、模板、静态资源按「功能包」切开,再在工厂里注册。
myapp/blog/__init__.py:
from flask import Blueprint
bp = Blueprint(
"blog",
__name__,
url_prefix="/blog",
template_folder="templates", # 可选:包内模板
)
from myapp.blog import routes # noqa: E402 注册视图
myapp/blog/routes.py:
from flask import render_template
from myapp.blog import bp
@bp.route("/")
def index():
return render_template("blog/index.html")
@bp.route("/<int:post_id>")
def detail(post_id):
return render_template("blog/detail.html", post_id=post_id)
工厂里:
def create_app(config_object="myapp.config.DevConfig"):
app = Flask(__name__)
app.config.from_object(config_object)
from myapp.blog import bp as blog_bp
app.register_blueprint(blog_bp)
return app
链接:url_for("blog.detail", post_id=1) → /blog/1。
经验法则:按领域拆(auth、blog、api),不要按「文件大小」随便切;API 可用独立 Blueprint + jsonify。
6. 配置、.env、flask run 与生产提示
6.1 配置类
myapp/config.py:
import os
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-me")
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL", "sqlite:///app.db"
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevConfig(Config):
DEBUG = True
class ProdConfig(Config):
DEBUG = False
6.2 .env(开发)
pip install python-dotenv
项目根 .env(不要提交密钥到 Git):
FLASK_APP=wsgi:app
SECRET_KEY=please-use-a-long-random-string
DATABASE_URL=sqlite:///app.db
flask run 在安装了 python-dotenv 时会自动加载 .env / .flaskenv。也可在工厂里:
from dotenv import load_dotenv
load_dotenv()
6.3 开发 vs 生产
| 场景 | 做法 |
|---|---|
| 本地开发 | flask run --debug(或 FLASK_DEBUG=1) |
| 生产 | 不要用内置服务器对外;用 Gunicorn / uWSGI 等 WSGI 服务器 |
| 密钥 | SECRET_KEY 用足够长的随机串,且每环境不同 |
| HTTPS | 由反向代理 / 平台终止 TLS |
| Debug | 生产关闭 debug,避免交互式调试器暴露 |
# 开发
flask run --host=127.0.0.1 --port=5000 --debug
7. SQLite + Flask-SQLAlchemy:CRUD 小例子
pip install flask-sqlalchemy
myapp/extensions.py:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
模型与路由(可放在 myapp/models.py / myapp/notes.py):
from datetime import datetime, timezone
from flask import Blueprint, request, redirect, url_for, render_template, abort
from myapp.extensions import db
bp = Blueprint("notes", __name__, url_prefix="/notes")
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
body = db.Column(db.Text, default="")
created_at = db.Column(
db.DateTime, default=lambda: datetime.now(timezone.utc)
)
@bp.route("/")
def list_notes():
notes = Note.query.order_by(Note.created_at.desc()).all()
return render_template("notes/list.html", notes=notes)
@bp.route("/new", methods=["GET", "POST"])
def create_note():
if request.method == "POST":
title = (request.form.get("title") or "").strip()
body = request.form.get("body") or ""
if not title:
abort(400)
note = Note(title=title, body=body)
db.session.add(note)
db.session.commit()
return redirect(url_for("notes.list_notes"))
return render_template("notes/form.html", note=None)
@bp.route("/<int:note_id>")
def read_note(note_id):
note = Note.query.get_or_404(note_id)
return render_template("notes/detail.html", note=note)
@bp.route("/<int:note_id>/edit", methods=["GET", "POST"])
def update_note(note_id):
note = Note.query.get_or_404(note_id)
if request.method == "POST":
note.title = (request.form.get("title") or "").strip() or note.title
note.body = request.form.get("body") or ""
db.session.commit()
return redirect(url_for("notes.read_note", note_id=note.id))
return render_template("notes/form.html", note=note)
@bp.route("/<int:note_id>/delete", methods=["POST"])
def delete_note(note_id):
note = Note.query.get_or_404(note_id)
db.session.delete(note)
db.session.commit()
return redirect(url_for("notes.list_notes"))
工厂里初始化:
from myapp.extensions import db
def create_app(config_object="myapp.config.DevConfig"):
app = Flask(__name__)
app.config.from_object(config_object)
db.init_app(app)
from myapp.notes import bp as notes_bp
app.register_blueprint(notes_bp)
with app.app_context():
db.create_all() # 演示用;正式项目用迁移(Flask-Migrate/Alembic)
return app
list.html 片段:
{% for n in notes %}
<article>
<a href="{{ url_for('notes.read_note', note_id=n.id) }}">{{ n.title }}</a>
</article>
{% else %}
<p>还没有笔记。</p>
{% endfor %}
<a href="{{ url_for('notes.create_note') }}">新建</a>
SQLite 文件路径注意:sqlite:///app.db 相对当前工作目录;部署时用绝对路径或平台提供的磁盘/托管数据库。
8. Session / Cookie / 简易登录
Flask 的 session 是签名的 Cookie(默认),不是服务端存储;改 SECRET_KEY 会使旧 session 失效。
from functools import wraps
from flask import session, request, redirect, url_for, render_template, flash
def login_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get("user_id"):
return redirect(url_for("auth.login", next=request.path))
return view(*args, **kwargs)
return wrapped
@bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username") or ""
password = request.form.get("password") or ""
# 演示:硬编码;实战用哈希(Werkzeug generate_password_hash / check_password_hash)
if username == "admin" and password == "admin":
session.clear()
session["user_id"] = 1
session["username"] = username
nxt = request.args.get("next") or url_for("index")
return redirect(nxt)
flash("用户名或密码错误", "error")
return render_template("auth/login.html")
@bp.route("/logout", methods=["POST"])
def logout():
session.clear()
return redirect(url_for("index"))
@bp.route("/me")
@login_required
def me():
return f"你好,{session.get('username')}"
设置 Cookie(非 session):
from flask import make_response
@app.route("/theme/<name>")
def set_theme(name):
resp = make_response(redirect(url_for("index")))
resp.set_cookie(
"theme",
name,
max_age=30 * 24 * 3600,
httponly=True,
samesite="Lax",
# secure=True, # 仅 HTTPS 时打开
)
return resp
安全提示:生产用强 SECRET_KEY;密码只存哈希;登录表单加 CSRF(Flask-WTF);敏感 Cookie 加 Secure / HttpOnly / 合适的 SameSite。
9. 错误页、日志、测试(pytest)
9.1 错误页
from flask import render_template
@app.errorhandler(404)
def not_found(e):
return render_template("errors/404.html"), 404
@app.errorhandler(500)
def server_error(e):
return render_template("errors/500.html"), 500
9.2 日志
import logging
from logging.handlers import RotatingFileHandler
def configure_logging(app):
if not app.debug and not app.testing:
handler = RotatingFileHandler(
"app.log", maxBytes=1_000_000, backupCount=3
)
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
app.logger.info("app started")
开发时 flask run --debug 已有请求日志;生产交给进程管理器 / 平台采集 stdout。
9.3 pytest(可运行最小例)
pip install pytest
tests/conftest.py:
import pytest
from myapp import create_app
from myapp.extensions import db
@pytest.fixture
def app():
app = create_app("myapp.config.DevConfig")
app.config.update(
{
"TESTING": True,
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
"WTF_CSRF_ENABLED": False,
"SECRET_KEY": "test",
}
)
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
tests/test_notes.py:
def test_list_empty(client):
rv = client.get("/notes/")
assert rv.status_code == 200
def test_create_note(client):
rv = client.post(
"/notes/new",
data={"title": "hello", "body": "world"},
follow_redirects=True,
)
assert rv.status_code == 200
assert b"hello" in rv.data
pytest -q
更多见官方 Testing Flask Applications。
10. 部署到免费档:思路简述(不绑付费)
原则:应用用 Gunicorn(或等价 WSGI)跑,前面用反向代理 / 平台边缘提供 HTTPS;SQLite 在多实例或无持久盘时不可靠,免费档可先单实例 + 持久卷,或换平台免费 Postgres。
10.1 本机 / VPS 思路:Gunicorn + Cloudflare Tunnel
pip install gunicorn
gunicorn -w 2 -b 127.0.0.1:8000 wsgi:app
Cloudflare Tunnel(cloudflared)可把内网 localhost:8000 暴露到你的域名,免开公网端口、顺带 HTTPS。适合演示、个人小工具;进程守护用 systemd,密钥走环境变量。
10.2 Render / Fly.io 简述
- Render:连 Git 仓库,选 Web Service,启动命令例如
gunicorn wsgi:app,设环境变量SECRET_KEY等;免费档有休眠与限制,适合 demo。 - Fly.io:用
Dockerfile或 buildpack 部署容器,同样跑 Gunicorn;注意免费额度与机器休眠策略。
Dockerfile 示意(可按平台文档调整):
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn
COPY . .
ENV FLASK_APP=wsgi:app
CMD ["gunicorn", "-b", "0.0.0.0:8000", "wsgi:app"]
无论哪家:关 debug、强密钥、静态文件可走 CDN/平台、数据库与会话按平台限制选型。本文不绑定任何付费方案。
11. 常见坑与官方文档
常见坑
- 生产仍开 debug / 内置服务器对外 → 调试器与性能风险。
- 忘记
SECRET_KEY→ session / flash / CSRF 异常或可预测签名。 - 在 import 时创建全局
app上乱连扩展 → 测试与多 app 困难;优先工厂 +init_app。 - 硬编码 URL → 用
url_for;改路径时少踩坑。 - SQLite 路径 / 多 worker 写库 → 工作目录变化导致「找不到库」;多进程写 SQLite 易锁。
- 模板注入 → Jinja 默认转义,慎用
|safe;用户 HTML 要消毒。 - 只拷贝视图不注册 Blueprint → 路由 404。
- 在请求外用
session/url_for/db.session→ 需要应用或请求上下文(app.app_context())。 - CORS / Cookie 跨站 → 前后端分离时单独配置;
SameSite与 HTTPS 要一起想。 - 依赖没进
requirements.txt→ 线上 ImportError。
官方与延伸链接
- Flask 文档(Stable):https://flask.palletsprojects.com/en/stable/
- Quickstart:https://flask.palletsprojects.com/en/stable/quickstart/
- Tutorial(官方大型教程):https://flask.palletsprojects.com/en/stable/tutorial/
- Application Factory / Blueprints:Patterns
- Testing:https://flask.palletsprojects.com/en/stable/testing/
- Deploying:https://flask.palletsprojects.com/en/stable/deploying/
- Flask-SQLAlchemy:https://flask-sqlalchemy.palletsprojects.com/
- Flask-WTF:https://flask-wtf.readthedocs.io/
- Jinja2:https://jinja.palletsprojects.com/
- Werkzeug(密码哈希等):https://werkzeug.palletsprojects.com/
把上面的工厂、Blueprint、笔记 CRUD 与登录串起来,就是一个可继续长成「个人笔记站」的骨架。下一步通常是:Flask-Migrate 做迁移、把用户存进数据库、API Blueprint 与前端分离,以及按官方 Deploying换成真正的进程模型。
评论