2
Flask 3.x 实战
Flask: The Micro Framework
Flask 像瑞士军刀:核心就"路由 + 请求/响应 + 模板",其他全靠插件。这章从第一个 Hello World 讲到一个能跑的博客系统。
安装与第一个应用
pip install flask # 装 Flask 3.x
# app.py —— 这就是一个完整的 Flask 应用
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "你好,Flask!"
if __name__ == "__main__":
app.run(debug=True) # 开发模式,改代码自动重启
运行
python app.py
# * Running on http://127.0.0.1:5000
# 或者用命令行:flask --app app run --debug
路由:动态路由、url_for、redirect、abort
from flask import Flask, url_for, redirect, abort
app = Flask(__name__)
# 动态路由:<类型:变量名>,类型有 int/float/string/path
@app.route("/user/<int:uid>")
def profile(uid):
return f"用户 {uid}"
@app.route("/go-home")
def go_home():
# url_for 用函数名反查 URL,改路由不用改代码
return redirect(url_for("index"))
@app.route("/admin")
def admin():
abort(403) # 直接抛出 HTTP 错误状态码
请求与响应:request / response
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/hello")
def hello():
name = request.args.get("name", "陌生人") # ?name=xxx
ua = request.headers.get("User-Agent")
return jsonify({"msg": f"你好 {name}", "ua": ua})
@app.route("/api/login", methods=["POST"])
def login():
data = request.get_json() # 解析 JSON 请求体
return jsonify({"ok": True, "user": data["username"]})
请求对象 request 全解
| request.xxx | 拿什么 |
request.args | URL 查询串 ?a=1&b=2 |
request.form | POST 表单字段 |
request.get_json() | JSON 请求体 |
request.files | 上传的文件 |
request.cookies | 客户端 cookie |
request.headers | 请求头字典 |
request.method | GET/POST |
request.path / request.url | 路径 / 完整 URL |
响应对象与 jsonify
from flask import make_response, jsonify
# 普通返回字符串就是 200 + text/html
@app.route("/text")
def text():
return "你好"
# jsonify 自动设 Content-Type: application/json
@app.route("/api")
def api():
return jsonify({"ok": True, "data": [1, 2]})
# make_response 自定义状态码和头
@app.route("/custom")
def custom():
resp = make_response("Created", 201)
resp.headers["X-Custom"] = "hi"
return resp
静态文件
# Flask 默认把 static/ 目录挂在 /static
# 模板里引用:{{ url_for('static', filename='style.css') }}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
# 生产:静态文件交给 Nginx,别让 Flask 处理
Jinja2 模板:render_template 与继承
项目结构:templates/base.html、templates/index.html
<!-- templates/base.html 父模板 -->
<!DOCTYPE html>
<html><body>
<h1>我的网站</h1>
{% raw %}{% block content %}{% endblock %}{% endraw %}
</body></html>
{# templates/index.html 子模板继承父模板 #}
{% raw %}{% extends "base.html" %}
{% block content %}
<h2>{{ title }}</h2>
<ul>
{% for item in items %}
<li>{{ item.name }} - {{ item.price }}元</li>
{% endfor %}
</ul>
{% endblock %}{% endraw %}
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/shop")
def shop():
items = [{"name": "苹果", "price": 5},
{"name": "香蕉", "price": 3}]
return render_template("index.html", title="水果摊", items=items)
数据库:Flask-SQLAlchemy 与迁移
pip install flask-sqlalchemy flask-migrate
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"
db = SQLAlchemy(app)
migrate = Migrate(app, db) # 命令行 flask db init/migrate/upgrade
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120))
body = db.Column(db.Text)
# CRUD 速查
p = Post(title="第一篇", body="你好")
db.session.add(p); db.session.commit()
Post.query.filter_by(title="第一篇").all()
Post.query.get(1)
p.title = "改了"; db.session.commit()
db.session.delete(p); db.session.commit()
应用工厂模式(大型项目必备)
# 在函数里创建 app,方便测试和多环境配置
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy() # 先创建扩展,后 init_app
def create_app(config_name="default"):
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db.init_app(app) # 扩展绑定到 app
from .blueprints.main import main_bp
app.register_blueprint(main_bp)
return app
# 这样测试时可以 create_app(test_config) 用临时数据库
蓝图 Blueprint:把大应用拆成模块
# blueprints/auth.py
from flask import Blueprint, render_template
auth = Blueprint("auth", __name__, url_prefix="/auth")
@auth.route("/login")
def login():
return render_template("login.html")
# app.py 注册蓝图
app.register_blueprint(auth)
中间件钩子大全
| 钩子 | 时机 / 用途 |
before_request | 请求进来、进视图前(鉴权、打日志) |
after_request | 视图返回后、发响应前(加头、统一格式) |
teardown_request | 响应发出后(清理,即使出错也跑) |
before_first_request | 第一次请求前(初始化,3.0 已移除) |
中间件:before_request / after_request
@app.before_request
def log_request():
print(f"收到请求 {request.path}")
@app.after_request
def add_header(response):
response.headers["X-App"] = "flask-blog"
return response
Jinja2 常用过滤器(管道符 | 后面跟过滤器)
| 过滤器 | 作用 |
{{ x|upper }} | 转大写 |
{{ x|truncate(20) }} | 截断到 20 字符 |
{{ x|default("无") }} | 空值给默认 |
{{ x|safe }} | 不转义 HTML(确认安全才用) |
{{ x|date("Y-m-d") }} | 格式化日期 |
表单处理与 CSRF 保护(Flask-WTF)
pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, validators
class PostForm(FlaskForm):
title = StringField("标题", [validators.InputRequired(), validators.Length(max=120)])
body = TextAreaField("正文", [validators.InputRequired()])
submit = SubmitField("发布")
@app.route("/new", methods=["GET", "POST"])
def new_post():
form = PostForm()
if form.validate_on_submit(): # POST 且校验通过
p = Post(title=form.title.data, body=form.body.data)
db.session.add(p); db.session.commit()
return redirect(url_for("post_detail", pid=p.id))
return render_template("new.html", form=form)
SECRET_KEY 别写死
Flask 的 session、WTF CSRF 都靠 app.secret_key 签名。生产环境从环境变量读:app.secret_key = os.environ["SECRET_KEY"],别提交到 git。泄露了等于别人能伪造你的 session。
会话、登录认证与 JWT
# session:把小数据存在客户端 cookie(签名过)
from flask import session
@app.route("/login", methods=["POST"])
def login():
session["user_id"] = user.id # 写 session,下次请求能读到
return {"ok": True}
# Flask-Login:管理登录状态,@login_required 装饰器保护视图
# pip install flask-login
from flask_login import LoginManager, login_required, login_user, current_user
login_manager = LoginManager()
login_manager.init_app(app)
@app.route("/dashboard")
@login_required
def dashboard():
return f"你好 {current_user.name}"
配置管理与环境变量
# config.py:开发/生产配置分离
class BaseConfig:
SECRET_KEY = "dev"
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevConfig(BaseConfig):
DEBUG = True
class ProdConfig(BaseConfig):
DEBUG = False
SECRET_KEY = os.environ["SECRET_KEY"]
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
app.config.from_object(ProdConfig)
配置管理与错误处理
# 配置:开发/生产分开,读环境变量
import os
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-only")
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"DATABASE_URL", "sqlite:///dev.db")
# 统一错误处理:errorhandler
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404
@app.errorhandler(500)
def server_error(e):
return {"error": "服务器开小差了"}, 500
错误处理 errorhandler
# 统一处理 404/500,别让用户看到裸栈追踪
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404
@app.errorhandler(500)
def server_error(e):
app.logger.exception("500: %s", e)
return jsonify({"error": "服务器开小差了"}), 500
# abort() 主动抛错:abort(403) 直接跳到 errorhandler(403)
日志与错误追踪
# Flask 默认把错误打到 stderr,生产要配 logging
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler()],
)
@app.errorhandler(500)
def internal_error(e):
app.logger.exception("服务器错误") # 打完整堆栈
return "服务器开小差了", 500
# 生产建议:用 Sentry 或 Datadog 收错误,别只看 stdout
部署:Gunicorn + Nginx + Docker
# 生产启动(4 个 worker,绑 8000 端口)
gunicorn -w 4 -b 127.0.0.1:8000 app:app
# Nginx 反向代理(静态文件给 Nginx,动态转发给 Gunicorn)
# server {
# location /static/ { alias /path/to/static/; }
# location / { proxy_pass http://127.0.0.1:8000; }
# }
# Dockerfile 示例
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:app"]
完整案例:Flask 博客系统(目录结构 + 核心代码)
blog/
├── app.py # 入口,注册蓝图、初始化扩展
├── models.py # Post/Comment/User 模型
├── templates/
│ ├── base.html
│ ├── list.html # 文章列表+分页
│ └── detail.html # 文章详情+评论
└── static/ # CSS/JS
# models.py
from datetime import datetime
from app import db
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
body = db.Column(db.Text)
created = db.Column(db.DateTime, default=datetime.utcnow)
comments = db.relationship("Comment", backref="post", lazy=True)
class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(300))
post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
# app.py 核心路由:列表+分页+详情+评论
from flask import Flask, render_template, request, redirect, url_for
from models import db, Post, Comment
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"
db.init_app(app)
@app.route("/")
def post_list():
page = request.args.get("page", 1, type=int)
pagination = Post.query.order_by(Post.created.desc()).paginate(page=page, per_page=5)
return render_template("list.html", pagination=pagination)
@app.route("/post/<int:pid>")
def post_detail(pid):
post = Post.query.get_or_404(pid)
return render_template("detail.html", post=post)
@app.route("/post/<int:pid>/comment", methods=["POST"])
def add_comment(pid):
c = Comment(body=request.form["body"], post_id=pid)
db.session.add(c); db.session.commit()
return redirect(url_for("post_detail", pid=pid))
with app.app_context(): # 第一次跑建表
db.create_all()
templates/detail.html 文章详情 + 评论表单
{% raw %}{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p class="meta">{{ post.created.strftime("%Y-%m-%d") }}</p>
<div class="body">{{ post.body }}</div>
<h3>评论 ({{ post.comments|length }})</h3>
{% for c in post.comments %}
<p>{{ c.body }}</p>
{% endfor %}
<form method="post" action="/post/{{ post.id }}/comment">
<textarea name="body" required></textarea>
<button>发表评论</button>
</form>
{% endblock %}{% endraw %}
运行
flask --app app run --debug
# * Running on http://127.0.0.1:5000
# 浏览器打开 / 看到文章列表,点进 /post/1 看详情并评论
生产环境别用 app.run()
app.run() 是开发服务器,单线程、慢、不安全。生产用 Gunicorn:gunicorn -w 4 -b 0.0.0.0:8000 app:app,前面套 Nginx 处理静态文件和 HTTPS。debug=True 在生产必须关掉——它会暴露调试器,等于把服务器密码贴在公网上。
本章面试题 · Flask
1.(概念题)Flask 里 app_context 和 request_context 有什么区别?
查看答案
答案:request_context 是每个 HTTP 请求一个,含 request/session;app_context 是应用级,绑定 current_app。在请求外(比如命令行、后台线程)用 db.session 之前,必须 with app.app_context():,否则报 "Working outside of application context"。
2.(概念题)Flask 的 request.args 和 request.form 有什么区别?
查看答案
答案:args 取 URL 查询串(?a=1),form 取 POST 表单(application/x-www-form-urlencoded);JSON 请求体用 request.get_json()。三者对应三种数据来源,别拿错。
3.(思考题)Flask 为什么是"微框架"?和 Django 比,大项目会遇到什么问题?
查看答案
答案:Flask 核心只做路由+请求响应+模板,ORM/表单/认证/后台全靠插件。小项目自由,大项目要自己拼规范、选插件版本、统一目录——拼不好就乱。这就是大项目倾向 Django 的原因。