Flask 教程
Flask 是 Python 的轻量级 Web 框架,由 Armin Ronacher 于 2010 年发布。它以"简单、灵活、可扩展"著称,核心只有路由和模板,其余功能通过扩展实现——适合快速开发和小型到中型应用。
Flask 的特点
| 特点 | 说明 |
|---|---|
| 轻量 | 核心极小,一个文件就能跑起来 |
| 灵活 | 不强制项目结构,自由组织 |
| 扩展丰富 | Flask-SQLAlchemy、Flask-Login 等 |
| 内置模板 | Jinja2 模板引擎 |
| 开发调试 | 自带开发服务器与调试器 |
最小应用
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, CodeNest!"
if __name__ == "__main__":
app.run(debug=True)
# 运行
python app.py
# 访问 http://127.0.0.1:5000
Flask vs FastAPI vs Django
| 框架 | 定位 | 适用 |
|---|---|---|
| Flask | 轻量灵活 | 小中型、快速原型 |
| FastAPI | 高性能 API | 前后端分离、高并发 |
| Django | 大而全 | 大型全栈应用 |
Flask 的生态
- Flask-SQLAlchemy:ORM 数据库
- Flask-Login:用户认证
- Flask-Migrate:数据库迁移
- Flask-WTF:表单
- Flask-RESTful:API 扩展
Flask 学习曲线平缓,适合作为 Python Web 入门框架;理解它的路由/模板/请求响应模型后,迁移到其他框架很容易。
Flask 简介
Flask 简介:Flask 的历史、设计理念与核心概念。
Flask 的诞生
- 2010 年由 Armin Ronacher 创建(Werkzeug 团队)
- 源于对微型框架的需求("micro-framework")
- 建立在 Werkzeug(WSGI)和 Jinja2(模板)之上
"微型"的含义
Flask 的"micro"不是说功能少,而是核心精简:默认只提供路由、模板、请求响应;数据库、认证、表单等通过扩展按需添加。
Flask 的核心组成
| 组件 | 作用 |
|---|---|
| Werkzeug | WSGI 工具库(路由、请求响应) |
| Jinja2 | 模板引擎 |
| click | 命令行接口(flask run) |
| itsdangerous | 签名(Session 安全) |
Flask 的优势
- 上手快:半小时写个 Web 应用
- 灵活:项目结构自由
- 文档优秀:官方文档清晰完整
- 社区大:问题容易找到答案
典型使用场景
- 个人网站、博客
- 内部管理系统
- API 服务(配合前端)
- 机器学习模型部署
- 快速原型验证
Flask 的理念:"微核心 + 扩展生态"。你掌控一切,框架不替你做决定。
Flask 安装
安装 Flask 并搭建开发环境。
环境要求
- Python 3.8+
- pip
- 推荐虚拟环境
创建虚拟环境
python3 -m venv venv
source venv/bin/activate # macOS/Linux
venv\Scripts\activate # Windows
安装 Flask
pip install flask
# 验证
python -c "import flask; print(flask.__version__)"
# 3.x
常用扩展安装
pip install flask-sqlalchemy # ORM
pip install flask-migrate # 迁移
pip install flask-login # 认证
pip install flask-wtf # 表单
pip install flask-cors # 跨域
第一个应用
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello Flask!"
if __name__ == "__main__":
app.run(debug=True)
运行方式
# 方式一:直接运行
python app.py
# 方式二:flask 命令
export FLASK_APP=app.py
export FLASK_DEBUG=1
flask run
常见问题
| 问题 | 解决 |
|---|---|
| 端口被占用 | app.run(port=5001) |
| 局域网访问 | app.run(host="0.0.0.0") |
| 修改后不生效 | 开启 debug=True 自动重载 |
始终在虚拟环境中开发;保存依赖:pip freeze > requirements.txt。
Flask 第一个应用
创建你的第一个 Flask 应用,走通完整流程。
编写代码
# app.py
from flask import Flask, request
app = Flask(__name__)
# 路由 + 视图函数
@app.route("/")
def home():
return "<h1>欢迎来到 CodeNest</h1>"
@app.route("/hello/<name>")
def hello(name):
return f"你好,{name}!"
@app.route("/api/data")
def data():
return {"message": "这是 JSON 数据", "count": 42}
if __name__ == "__main__":
app.run(debug=True)
启动与访问
python app.py
# * Running on http://127.0.0.1:5000
# 访问:
# http://127.0.0.1:5000/ → 欢迎页
# http://127.0.0.1:5000/hello/张三 → 你好,张三!
# http://127.0.0.1:5000/api/data → JSON
代码解析
| 代码 | 作用 |
|---|---|
Flask(__name__) | 创建应用实例 |
@app.route("/") | 注册路由 |
def home() | 视图函数,返回响应 |
debug=True | 调试模式(自动重载 + 报错页) |
返回 JSON
# 直接返回字典 → Flask 自动转 JSON
@app.route("/api/user")
def user():
return {
"id": 1,
"name": "张三",
"skills": ["Python", "Flask"]
}
# 响应头 Content-Type: application/json
debug 模式注意
# 生产环境必须关闭 debug!
if __name__ == "__main__":
app.run(debug=False) # 生产
# 或通过环境变量控制
第一个应用跑通后,理解三要素:路由(URL)→ 视图函数(逻辑)→ 响应(返回内容)。
Flask 基本概念
掌握 Flask 基本概念:请求、响应、路由、应用上下文。
Flask 的工作流程
浏览器请求 → WSGI → Flask 路由匹配 → 视图函数
↓
浏览器响应 ← WSGI ← 响应对象 ← 返回值处理
核心概念
| 概念 | 说明 |
|---|---|
| 应用(app) | Flask 实例,全局配置与路由 |
| 路由(route) | URL 到视图函数的映射 |
| 视图函数 | 处理请求并返回响应 |
| 请求(request) | 封装客户端请求数据 |
| 响应(response) | 返回给客户端的数据 |
| 上下文(context) | 请求/应用上下文,供全局访问 |
request 对象
from flask import request
@app.route("/info")
def info():
return {
"method": request.method, # GET/POST
"path": request.path, # /info
"args": request.args.to_dict(),# 查询参数
"headers": dict(request.headers), # 请求头
"ip": request.remote_addr, # 客户端 IP
}
响应返回方式
from flask import Response, jsonify
# 1. 字符串
return "Hello"
# 2. 字典(自动 JSON)
return {"ok": True}
# 3. 元组(内容, 状态码)
return "Not Found", 404
# 4. Response 对象
resp = Response("内容", status=200)
resp.headers["X-Custom"] = "value"
return resp
应用上下文
# current_app / g 在请求中全局可用
from flask import current_app, g
@app.before_request
def before():
g.start_time = time.time() # 存到 g
@app.route("/slow")
def slow():
# 视图内可访问 g
elapsed = time.time() - g.start_time
return f"处理耗时 {elapsed:.3f}s"
理解"请求 → 上下文 → 视图 → 响应"这条链路,Flask 的一切都围绕它运转。
Flask 项目结构
掌握 Flask 项目结构:从小单文件到工程化组织。
小项目:单文件
app.py # 全部代码
中项目:模块化
myapp/
├── app.py # 入口
├── config.py # 配置
├── models.py # 数据模型
├── views/ # 视图
│ ├── __init__.py
│ ├── main.py
│ └── api.py
├── templates/ # Jinja2 模板
├── static/ # 静态文件(css/js/img)
└── requirements.txt
大型项目:工厂模式
myapp/
├── run.py # 启动入口
├── config.py # 配置(开发/生产)
├── app/
│ ├── __init__.py # create_app 工厂
│ ├── models/ # 模型
│ ├── views/ # 蓝图
│ ├── templates/
│ ├── static/
│ └── extensions.py # 扩展初始化
├── migrations/ # 数据库迁移
└── requirements.txt
工厂函数模式(create_app)
# app/__init__.py
from flask import Flask
def create_app(config_name="default"):
app = Flask(__name__)
app.config.from_object(config[config_name])
# 初始化扩展
db.init_app(app)
# 注册蓝图
from .views import main
app.register_blueprint(main.bp)
return app
# run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
结构选择建议
- 原型/小项目:单文件
- 中型:模块化目录
- 可扩展/团队:工厂 + 蓝图 + 扩展分离
项目变大后第一时间拆蓝图(Blueprint):按功能模块组织路由,避免单文件无限膨胀。
Flask 路由
掌握 Flask 路由:URL 映射、动态参数、HTTP 方法。
基本路由
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "首页"
@app.route("/about")
def about():
return "关于页"
动态路由(URL 参数)
# <参数名> 捕获 URL 片段
@app.route("/user/<username>")
def user(username):
return f"用户:{username}"
# 带类型转换器
@app.route("/post/<int:post_id>")
def post(post_id):
return f"文章 ID:{post_id}" # 自动转 int
@app.route("/files/<path:filepath>")
def file(filepath):
return f"文件路径:{filepath}" # path 可含斜杠
类型转换器
| 转换器 | 匹配 | 示例 |
|---|---|---|
string | 字符串(默认) | <string:name> |
int | 整数 | <int:id> |
float | 浮点数 | <float:price> |
path | 含斜杠路径 | <path:path> |
uuid | UUID | <uuid:uid> |
HTTP 方法
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
return "提交登录"
return "显示登录表单"
# 或
@app.get("/data") # 只 GET
def get_data(): ...
@app.post("/data") # 只 POST
def post_data(): ...
url_for(反向生成 URL)
from flask import url_for
@app.route("/user/<int:uid>")
def user(uid):
return f"用户 {uid}"
# 视图中生成 URL
url = url_for("user", uid=42) # "/user/42"
# 模板中使用
# <a href="{{ url_for('user', uid=42) }}">查看</a>
路由命名(函数名)要清晰,配合 url_for() 使用——改 URL 时只需改一处。
Flask 请求与响应
掌握 Flask 请求与响应:完整的数据交互。
请求数据(request)
from flask import request
# 查询参数(GET)
request.args.get("page", default=1, type=int) # ?page=2
request.args.get("q")
# 表单数据(POST)
request.form.get("username") # application/x-www-form-urlencoded
request.form.getlist("tags") # 多值
# JSON 数据(POST)
data = request.get_json() # 返回 dict/None
data.get("name")
# 请求头
request.headers.get("Authorization")
request.headers.get("User-Agent")
# 文件上传
file = request.files.get("file")
file.filename
file.save("/tmp/uploaded.jpg")
# 其他
request.method # 请求方法
request.path # 路径
request.remote_addr # IP
request.cookies.get("key") # Cookie
响应数据
from flask import jsonify, redirect, url_for, make_response
# JSON
return jsonify({"status": "ok", "data": [1, 2, 3]})
# 重定向
return redirect(url_for("index"))
return redirect("https://example.com")
# 状态码 + 响应头
resp = make_response("内容")
resp.status_code = 404
resp.headers["X-Powered-By"] = "CodeNest"
return resp
# 设置 Cookie
resp = make_response("ok")
resp.set_cookie("theme", "dark", max_age=3600)
return resp
# 文件下载
from flask import send_file
return send_file("report.pdf", as_attachment=True)
完整示例
@app.post("/api/users")
def create_user():
data = request.get_json()
if not data or not data.get("name"):
return jsonify({"error": "缺少 name"}), 400
# 业务逻辑...
return jsonify({"id": 1, "name": data["name"]}), 201
request 取数据、返回值定义响应——理解这两个对象,就能处理 90% 的接口开发。
Flask 视图函数
掌握 Flask 视图函数:组织业务逻辑的入口。
视图函数基础
from flask import Flask, request, abort
app = Flask(__name__)
@app.route("/")
def index():
return "首页"
# 带逻辑的视图
@app.route("/search")
def search():
keyword = request.args.get("q", "")
if not keyword:
abort(400, "缺少搜索词")
# 业务逻辑...
return f"搜索:{keyword}"
视图返回类型
from flask import jsonify, render_template, redirect
# 返回模板
@app.route("/page")
def page():
return render_template("page.html", title="页面")
# 返回 JSON
@app.route("/api/data")
def api():
return jsonify({"ok": True})
# 返回重定向
@app.route("/old")
def old():
return redirect("/new")
错误处理
from flask import abort
@app.route("/user/<int:uid>")
def user(uid):
if uid <= 0:
abort(404) # 404 页面
if uid > 10000:
abort(403, "无权访问") # 403 + 描述
return f"用户 {uid}"
# 自定义错误页面
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "页面不存在"}), 404
@app.errorhandler(500)
def server_error(e):
return jsonify({"error": "服务器错误"}), 500
请求钩子(before/after)
@app.before_request
def before_request():
# 每个请求前执行(鉴权、日志)
pass
@app.after_request
def after_request(response):
# 每个响应后执行(加头、统计)
response.headers["X-Server"] = "Flask"
return response
视图函数保持"薄":只做参数解析 + 调用业务 + 返回响应;复杂逻辑抽到 service 层。
Flask 模板渲染
掌握 Flask 模板渲染:Jinja2 模板引擎。
渲染模板
from flask import render_template
@app.route("/")
def index():
return render_template("index.html", title="首页", users=users)
模板文件结构
templates/
├── index.html
├── user/
│ └── profile.html
└── base.html
Jinja2 语法
<!-- 输出变量 -->
<h1>{{ title }}</h1>
<!-- if 判断 -->
{% if user %}
<p>你好,{{ user.name }}</p>
{% else %}
<p>请登录</p>
{% endif %}
<!-- for 循环 -->
<ul>
{% for u in users %}
<li>{{ u.name }} - {{ u.age }}岁</li>
{% endfor %}
</ul>
<!-- 过滤器 -->
{{ name | upper }}
{{ content | truncate(50) }}
{{ price | round(2) }}
<!-- 注释 -->
{# 这是模板注释 #}
模板继承
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}默认标题{% endblock %}</title>
</head>
<body>
<nav>网站导航</nav>
{% block content %}{% endblock %}
<footer>版权信息</footer>
</body>
</html>
<!-- index.html -->
{% extends "base.html" %}
{% block title %}首页{% endblock %}
{% block content %}
<h1>欢迎</h1>
{% include "partials/list.html" %}
{% endblock %}
模板中的 url_for
<a href="{{ url_for('index') }}">首页</a>
<a href="{{ url_for('user', uid=42) }}">用户</a>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
模板自动转义变量(防 XSS);确需输出 HTML 用 |safe(仅限可信内容)。模板继承避免重复 HTML。
Flask 静态文件
掌握 Flask 静态文件:CSS、JS、图片的托管。
静态文件目录
static/
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
模板中引用
<link rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}">
自定义静态目录
app = Flask(__name__,
static_folder="assets", # 目录名
static_url_path="/assets") # URL 前缀
# 访问 /assets/css/style.css
文件下载
from flask import send_file, send_from_directory
@app.route("/download/<filename>")
def download(filename):
# 从目录发送文件(安全)
return send_from_directory("uploads", filename, as_attachment=True)
@app.route("/download2")
def download2():
return send_file("static/files/report.pdf", as_attachment=True)
上传文件
import os
from flask import request
UPLOAD_FOLDER = "uploads"
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
@app.post("/upload")
def upload():
file = request.files.get("file")
if not file:
return {"error": "未上传文件"}, 400
# 安全处理文件名
filename = secure_filename(file.filename)
path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
file.save(path)
return {"saved": filename, "url": f"/uploads/{filename}"}
上传文件名务必用 secure_filename() 清洗,防路径穿越;生产环境静态文件用 Nginx 托管更高效。
Flask Session 与 Cookie
掌握 Flask Session 与 Cookie:状态保持。
Cookie 操作
from flask import make_response, request
@app.route("/set-cookie")
def set_cookie():
resp = make_response("设置成功")
resp.set_cookie("theme", "dark",
max_age=3600, # 1 小时
httponly=True, # JS 不可读
path="/")
return resp
@app.route("/get-cookie")
def get_cookie():
theme = request.cookies.get("theme", "light")
return f"主题:{theme}"
@app.route("/delete-cookie")
def delete_cookie():
resp = make_response("已删除")
resp.delete_cookie("theme")
return resp
Session 操作
from flask import session
# 需要密钥(签名保护)
app.secret_key = "your-secret-key-here"
# 生产环境用环境变量
# app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")
@app.route("/login")
def login():
session["user_id"] = 1
session["username"] = "张三"
session.permanent = True # 持久会话
return "已登录"
@app.route("/me")
def me():
username = session.get("username")
if not username:
return "未登录", 401
return f"当前用户:{username}"
@app.route("/logout")
def logout():
session.clear()
return "已退出"
Session 原理
Session 数据默认存客户端 Cookie(签名防篡改)
- 服务器不存 Session(轻量)
- 内容经过 itsdangerous 签名
- 敏感数据不要放 Session(可被读取,只是防篡改)
生产可换 Redis 等存储(Flask-Session 扩展)
会话配置
app.config.update(
SECRET_KEY="complex-secret",
SESSION_COOKIE_HTTPONLY=True, # 防 XSS 读取
SESSION_COOKIE_SAMESITE="Lax", # CSRF 防护
PERMANENT_SESSION_LIFETIME=86400, # 1 天
)
SECRET_KEY 必须保密且足够复杂(用环境变量注入);它泄露=可以伪造任意 Session。
Flask 表单处理
掌握 Flask 表单处理:接收、校验、Flask-WTF。
接收表单数据
from flask import request
@app.route("/submit", methods=["GET", "POST"])
def submit():
if request.method == "POST":
# 读取表单字段
name = request.form.get("name")
email = request.form.get("email")
agree = request.form.get("agree") == "on"
# 手动校验
if not name or len(name) < 2:
return {"error": "姓名至少 2 个字符"}, 400
return {"ok": True, "name": name}
return "GET 请求"
HTML 表单模板
<form method="post">
<input type="text" name="name" placeholder="姓名">
<input type="email" name="email" placeholder="邮箱">
<label><input type="checkbox" name="agree"> 同意协议</label>
<button type="submit">提交</button>
</form>
Flask-WTF(推荐)
pip install flask-wtf
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField
from wtforms.validators import DataRequired, Email, Length
class RegisterForm(FlaskForm):
username = StringField("用户名", validators=[
DataRequired(), Length(min=3, max=20)])
email = StringField("邮箱", validators=[
DataRequired(), Email()])
password = PasswordField("密码", validators=[
DataRequired(), Length(min=6)])
agree = BooleanField("同意协议", validators=[
DataRequired()])
@app.route("/register", methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit(): # 校验通过
username = form.username.data
return f"注册成功:{username}"
return render_template("register.html", form=form)
<form method="post">
{{ form.hidden_tag() }} <!-- CSRF 令牌 -->
{{ form.username.label }} {{ form.username() }}
{% for err in form.username.errors %}<p class="error">{{ err }}</p>{% endfor %}
{{ form.password.label }} {{ form.password() }}
{{ form.agree() }} 同意协议
<button type="submit">注册</button>
</form>
CSRF 保护
# Flask-WTF 默认开启 CSRF 保护
# 模板中必须加 {{ form.hidden_tag() }}
# 或配置 SECRET_KEY 后使用
app.config["SECRET_KEY"] = "xxx"
app.config["WTF_CSRF_ENABLED"] = True
简单表单手动解析即可;复杂表单用 Flask-WTF(自动校验 + CSRF 保护),生产必开 CSRF。
Flask 数据库操作
掌握 Flask 数据库操作:SQLite/MySQL 基础与 ORM。
原生 SQL(sqlite3)
import sqlite3
# 连接
conn = sqlite3.connect("app.db")
cur = conn.cursor()
# 建表
cur.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)''')
# 插入(参数化防注入)
cur.execute("INSERT INTO users (name, email) VALUES (?, ?)",
("张三", "zhang@example.com"))
conn.commit()
# 查询
cur.execute("SELECT * FROM users WHERE name = ?", ("张三",))
rows = cur.fetchall()
conn.close()
Flask-SQLAlchemy(ORM,推荐)
pip install flask-sqlalchemy
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# SQLite
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
# MySQL
# app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://user:pass@localhost/db"
db = SQLAlchemy(app)
# 模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
email = db.Column(db.String(100), unique=True)
def to_dict(self):
return {"id": self.id, "name": self.name, "email": self.email}
# 建表(首次)
with app.app_context():
db.create_all()
CRUD 操作
@app.post("/users")
def create_user():
data = request.get_json()
user = User(name=data["name"], email=data["email"])
db.session.add(user)
db.session.commit()
return user.to_dict(), 201
@app.get("/users")
def list_users():
users = User.query.all()
return [u.to_dict() for u in users]
@app.get("/users/<int:uid>")
def get_user(uid):
user = db.session.get(User, uid)
if not user:
return {"error": "用户不存在"}, 404
return user.to_dict()
@app.put("/users/<int:uid>")
def update_user(uid):
user = db.session.get(User, uid)
if not user:
return {"error": "用户不存在"}, 404
data = request.get_json()
user.name = data.get("name", user.name)
db.session.commit()
return user.to_dict()
@app.delete("/users/<int:uid>", methods=["DELETE"])
def delete_user(uid):
user = db.session.get(User, uid)
if not user:
return {"error": "用户不存在"}, 404
db.session.delete(user)
db.session.commit()
return {"ok": True}
常用查询
User.query.all() # 全部
User.query.filter_by(name="张三").first()
User.query.filter(User.age > 18).all()
User.query.order_by(User.id.desc()).limit(10).all()
User.query.count() # 数量
User.query.get_or_404(1) # 404 处理
ORM 自动参数化防注入;本站 CodeNest 数据库表结构(categories/tutorials/sections)就是 ORM 模型的典型示例。
Flask 蓝图
掌握 Flask 蓝图(Blueprint):模块化组织路由。
为什么用蓝图
- 按功能模块拆分路由
- 避免单文件膨胀
- 可复用模块(插件式开发)
- 团队并行开发
创建蓝图
# views/main.py
from flask import Blueprint, render_template
# 创建蓝图:名称 + 前缀
main_bp = Blueprint("main", __name__)
@main_bp.route("/")
def index():
return render_template("index.html")
@main_bp.route("/about")
def about():
return "关于页面"
# views/api.py
from flask import Blueprint, jsonify
api_bp = Blueprint("api", __name__, url_prefix="/api")
@api_bp.route("/users")
def users():
return jsonify([{"id": 1, "name": "张三"}])
@api_bp.route("/posts/<int:pid>")
def post(pid):
return jsonify({"id": pid})
注册蓝图
# app.py
from flask import Flask
from views.main import main_bp
from views.api import api_bp
app = Flask(__name__)
app.register_blueprint(main_bp)
app.register_blueprint(api_bp) # 所有路由前缀 /api
# 自定义前缀
# app.register_blueprint(api_bp, url_prefix="/v1")
蓝图内使用 url_for
# 蓝图名 + 端点名
url_for("main.index") # /
url_for("api.users") # /api/users
url_for("api.post", pid=42) # /api/posts/42
# 模板中
# <a href="{{ url_for('main.about') }}">关于</a>
蓝图模板与静态文件
# 蓝图可带自己的模板/静态目录
admin_bp = Blueprint("admin", __name__,
template_folder="templates",
static_folder="static")
蓝图命名规范:用 蓝图名_端点 区分同名函数;url_prefix 统一接口前缀(/api、/admin)。
Flask 错误处理
掌握 Flask 错误处理:统一异常与错误响应。
错误处理装饰器
from flask import jsonify
# 404
@app.errorhandler(404)
def not_found(e):
return jsonify({"code": 404, "error": "页面不存在"}), 404
# 405 方法不允许
@app.errorhandler(405)
def method_not_allowed(e):
return jsonify({"code": 405, "error": "方法不允许"}), 405
# 500 服务器错误
@app.errorhandler(500)
def server_error(e):
# 记录日志
app.logger.error(f"服务器错误: {e}")
return jsonify({"code": 500, "error": "服务器内部错误"}), 500
自定义业务异常
class BizError(Exception):
def __init__(self, code, message, status_code=400):
self.code = code
self.message = message
self.status_code = status_code
@app.errorhandler(BizError)
def handle_biz_error(e):
return jsonify({
"code": e.code,
"message": e.message
}), e.status_code
# 使用
@app.route("/api/pay")
def pay():
balance = 10
amount = 100
if balance < amount:
raise BizError(4001, "余额不足", 400)
return {"ok": True}
统一响应格式
# 封装统一响应
def ok(data=None, message="ok"):
return jsonify({"code": 0, "message": message, "data": data})
def fail(code, message, status_code=400):
return jsonify({"code": code, "message": message, "data": None}), status_code
@app.get("/api/users")
def users():
return ok([{"id": 1}])
@app.get("/api/nothing")
def nothing():
return fail(4040, "资源不存在", 404)
全局异常兜底
import traceback
@app.errorhandler(Exception)
def handle_exception(e):
# 完整堆栈进日志(不暴露给客户端)
traceback.print_exc()
return jsonify({"code": 500, "message": "服务器错误"}), 500
错误处理三原则:不向客户端泄露堆栈、统一错误格式、日志记录完整信息。
Flask 中间件和扩展
掌握 Flask 中间件和扩展:请求拦截与扩展使用。
before_request / after_request
from flask import request, g
import time
# 每个请求前
@app.before_request
def before_request():
g.start = time.time()
# 简单鉴权(排除公开路径)
public = {"/", "/login", "/static"}
if request.path not in public:
token = request.headers.get("Authorization", "")
if not token.startswith("Bearer "):
return {"error": "未认证"}, 401
# 每个请求后
@app.after_request
def after_request(response):
# 计算耗时
if hasattr(g, "start"):
elapsed = time.time() - g.start
response.headers["X-Process-Time"] = f"{elapsed:.3f}s"
return response
# 异常时(替代 after)
@app.teardown_request
def teardown(exception):
# 清理资源(数据库关闭等)
pass
常用扩展
| 扩展 | 用途 |
|---|---|
| Flask-SQLAlchemy | ORM |
| Flask-Migrate | 数据库迁移 |
| Flask-Login | 用户认证 |
| Flask-WTF | 表单 + CSRF |
| Flask-CORS | 跨域 |
| Flask-Caching | 缓存 |
| Flask-Limiter | 限流 |
Flask-CORS 示例
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # 全开放(开发)
# 生产限制来源
CORS(app, resources={
r"/api/*": {"origins": ["https://www.example.com"]}
})
Flask-Limiter 限流
from flask_limiter import Limiter
limiter = Limiter(app, default_limits=["200 per day"])
@app.route("/api/login")
@limiter.limit("5 per minute") # 登录接口限流
def login():
return {"ok": True}
before_request 做全局鉴权/日志;扩展按需安装(Flask 哲学:核心精简,扩展即插即用)。
Flask 部署
掌握 Flask 部署:生产环境上线。
生产部署架构
浏览器 → Nginx(静态+反代)→ Gunicorn(WSGI)→ Flask 应用
↓
MySQL/Redis
安装 Gunicorn
pip install gunicorn
# 启动(4 进程)
gunicorn -w 4 -b 127.0.0.1:8000 app:app
# 后台运行 + 日志
gunicorn -w 4 -b 127.0.0.1:8000 app:app --daemon --log-file gunicorn.log
生产配置要点
# 关闭调试!
app.config.update(
DEBUG=False,
SECRET_KEY=os.environ.get("SECRET_KEY"),
# 生产数据库
SQLALCHEMY_DATABASE_URI=os.environ.get("DB_URL"),
)
Nginx 配置
server {
listen 80;
server_name www.laowang.click;
# 静态文件
location /static/ {
alias /www/wwwroot/app/static/;
expires 7d;
}
# 动态请求
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
systemd 服务
# /etc/systemd/system/flaskapp.service
[Unit]
Description=Flask App
After=network.target
[Service]
WorkingDirectory=/www/wwwroot/app
ExecStart=/www/wwwroot/app/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app
Restart=always
EnvironmentFile=/www/wwwroot/app/.env
[Install]
WantedBy=multi-user.target
宝塔面板部署
- 创建 Python 项目,上传代码
- 安装依赖(pip install -r requirements.txt)
- 启动命令设为 gunicorn
- 外网映射(自动反代)
- 申请 SSL 证书
上线检查
curl http://127.0.0.1:8000/health # 健康检查
curl -I https://www.laowang.click/ # 状态码 200
生产三件套:Gunicorn(多进程)+ Nginx(反代静态)+ 环境变量配置。DEBUG 必须 False。
Flask 配置管理
掌握 Flask 配置管理:配置文件、环境变量、加载顺序。
配置方式
# 方式一:直接赋值
app.config["DEBUG"] = True
app.config["SECRET_KEY"] = "xxx"
# 方式二:update 批量
app.config.update(
DEBUG=True,
SECRET_KEY="xxx",
)
# 方式三:对象加载(推荐)
class Config:
SECRET_KEY = "xxx"
SQLALCHEMY_DATABASE_URI = "sqlite:///app.db"
app.config.from_object(Config)
# 方式四:文件加载
app.config.from_pyfile("config.py")
app.config.from_json("config.json")
多环境配置
# config.py
import os
class BaseConfig:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevConfig(BaseConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"
class ProdConfig(BaseConfig):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get("DB_URL", "")
config = {
"dev": DevConfig,
"prod": ProdConfig,
"default": DevConfig,
}
# app.py 工厂中
def create_app(env="default"):
app = Flask(__name__)
app.config.from_object(config[env])
return app
# 运行
app = create_app(os.environ.get("FLASK_ENV", "default"))
.env 文件(python-dotenv)
pip install python-dotenv
# .env(不入版本库!)
SECRET_KEY=complex-secret-value
DB_URL=mysql+pymysql://user:pass@localhost/app
DEBUG=False
from dotenv import load_dotenv
load_dotenv() # 加载 .env
import os
secret = os.environ.get("SECRET_KEY")
读取配置
# 视图中读取
from flask import current_app
current_app.config["SECRET_KEY"]
current_app.config.get("DEBUG")
# 模板中
# {{ config['DEBUG'] }}
配置管理铁律:密钥等敏感信息放环境变量/.env,绝不写死在代码;开发/生产配置分离。
Flask 数据库集成
掌握 Flask 数据库集成:SQLAlchemy 完整实践。
安装与初始化
pip install flask-sqlalchemy
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
定义模型
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
sort_order = db.Column(db.Integer, default=0)
# 一对多关系
tutorials = db.relationship("Tutorial", backref="category",
lazy="dynamic")
class Tutorial(db.Model):
id = db.Column(db.Integer, primary_key=True)
cat_id = db.Column(db.Integer,
db.ForeignKey("category.id"), nullable=False)
title = db.Column(db.String(100), nullable=False)
views = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def to_dict(self):
return {
"id": self.id,
"title": self.title,
"views": self.views,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
# 建表
with app.app_context():
db.create_all()
关系查询
# 一对一/多对一
tutorial = Tutorial.query.first()
tutorial.category.name # 正向
category = Category.query.first()
category.tutorials.all() # 反向(backref)
# 跨表过滤
Tutorial.query.filter(Tutorial.category.has(name="前端")).all()
# 多对多
# tags = db.Table(...) 关联表
# Tag.tutorials = db.relationship(secondary=tags)
查询进阶
from sqlalchemy import func, or_, desc
# 聚合
db.session.query(Category.id,
func.count(Tutorial.id)).outerjoin(Tutorial) .group_by(Category.id).all()
# 条件组合
Tutorial.query.filter(
or_(Tutorial.title.contains("Python"),
Tutorial.title.contains("Flask"))
).order_by(desc(Tutorial.views)).all()
# 分页
page = Tutorial.query.paginate(page=1, per_page=10)
page.items; page.total; page.pages; page.has_next
事务管理
try:
db.session.add(new_tutorial)
db.session.add(new_category)
db.session.commit()
except Exception:
db.session.rollback()
raise
本站 CodeNest 的数据库(categories/tutorials/sections)与本示例结构几乎一致,可以对照学习。
Flask 测试
掌握 Flask 测试:单元测试与接口测试。
安装 pytest
pip install pytest
测试客户端
# test_app.py
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("test")
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_index(client):
resp = client.get("/")
assert resp.status_code == 200
assert b"CodeNest" in resp.data
def test_user_api(client):
resp = client.get("/api/users")
assert resp.status_code == 200
data = resp.get_json()
assert isinstance(data, list)
def test_create_user(client):
resp = client.post("/api/users", json={"name": "张三"})
assert resp.status_code == 201
assert resp.get_json()["name"] == "张三"
def test_not_found(client):
resp = client.get("/nonexistent")
assert resp.status_code == 404
测试数据库
@pytest.fixture
def client():
app = create_app("test")
app.config.update(
TESTING=True,
SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",
)
with app.app_context():
db.create_all()
# 插入测试数据
db.session.add(User(name="张三"))
db.session.commit()
with app.test_client() as client:
yield client
with app.app_context():
db.drop_all()
测试登录认证
def test_require_auth(client):
resp = client.get("/api/me")
assert resp.status_code == 401
def test_login(client):
resp = client.post("/api/login", json={
"username": "admin", "password": "secret"})
assert resp.status_code == 200
token = resp.get_json()["token"]
resp = client.get("/api/me",
headers={"Authorization": f"Bearer {token}"})
assert resp.status_code == 200
运行测试
pytest -v # 详细输出
pytest -k "user" # 只跑含 user 的
pytest --cov # 覆盖率(需 pytest-cov)
测试三覆盖:正常流程、错误分支、边界条件。接入 CI(GitHub Actions)每次提交自动跑。
Flask 请求生命周期
掌握 Flask 请求生命周期:请求从进入到返回的完整流程。
生命周期全景
1. 请求到达 WSGI 服务器(Gunicorn)
2. Flask 创建 Request 对象
3. 推送请求上下文(request/session 可访问)
4. before_request 钩子(可提前返回)
5. 路由匹配 → 视图函数
6. 视图处理 → 返回值
7. after_request 钩子(处理响应)
8. 序列化响应返回客户端
9. teardown_request(清理资源)
10. 弹出请求上下文
各阶段示例
from flask import Flask, request, g
import time
app = Flask(__name__)
# 1-2. 请求进入
@app.before_request
def before_request():
g.start_time = time.time()
print(f"开始处理: {request.method} {request.path}")
# 3. 视图
@app.route("/api/data")
def data():
return {"message": "hello"}
# 4. 响应生成
@app.after_request
def after_request(response):
elapsed = time.time() - g.start_time
response.headers["X-Time"] = f"{elapsed:.3f}s"
print(f"完成处理: {response.status_code} ({elapsed:.3f}s)")
return response
# 5. 清理
@app.teardown_request
def teardown(exception):
# 关闭数据库连接等
print("请求结束,清理资源")
上下文对象
# 请求上下文(每个请求独立)
request # 请求数据
session # 会话
# 应用上下文(整个应用共享)
current_app # 当前应用
g # 请求内全局(跨函数共享)
# g 的使用:在 before_request 存,视图取
@app.before_request
def load_user():
g.user = User.query.first()
@app.route("/me")
def me():
return {"user": g.user.name}
手动推送上下文
# 在请求外访问 current_app/db
from app import create_app, db
app = create_app()
with app.app_context():
# 此处可访问 db 等
users = User.query.all()
理解生命周期就能掌控全局逻辑:before 做鉴权/日志,after 做统计/加头,teardown 做清理。
Flask 应用对象 API
掌握 Flask 应用对象 API:app 的配置与常用方法。
创建应用
from flask import Flask
# 基本创建
app = Flask(__name__)
# 自定义模板/静态目录
app = Flask(__name__,
template_folder="templates",
static_folder="static",
static_url_path="/static")
# 配置
app.config["DEBUG"] = True
app.secret_key = "secret"
app.name # 应用名
app.root_path # 应用根目录
app.url_map # 路由映射表
路由方法
# 注册路由
app.route("/")(view_func)
app.add_url_rule("/path", "endpoint_name", view_func,
methods=["GET", "POST"])
# 查看所有路由
for rule in app.url_map.iter_rules():
print(rule.rule, rule.endpoint, rule.methods)
请求钩子
app.before_request(fn) # 每个请求前
app.before_first_request(fn) # 首次请求前(Flask 2.2 移除)
app.after_request(fn) # 每个响应后
app.teardown_request(fn) # 请求结束清理
app.teardown_appcontext(fn) # 应用上下文结束
# 作为装饰器
@app.before_request
def hook():
pass
错误处理
@app.errorhandler(404)
def handle_404(e):
return "自定义 404", 404
@app.errorhandler(ValueError)
def handle_value(e):
return "参数错误", 400
上下文管理
with app.app_context():
# 访问 current_app、db
pass
with app.test_request_context("/api/data?page=2"):
# 模拟请求上下文
print(request.args.get("page"))
app 对象是 Flask 的中心:路由、配置、钩子、错误处理都挂在它上面。工厂函数(create_app)是大型项目的标准组织方式。
Flask 蓝图对象 API
掌握 Flask 蓝图对象 API:Blueprint 的完整用法。
创建蓝图
from flask import Blueprint
# 基本
bp = Blueprint("name", __name__)
# 常用参数
bp = Blueprint(
"api",
__name__,
url_prefix="/api", # URL 前缀
template_folder="templates",# 蓝图模板目录
static_folder="static", # 蓝图静态目录
)
蓝图常用属性/方法
bp.name # 蓝图名
bp.url_prefix # 前缀
bp.route("/path") # 添加路由
bp.errorhandler(404) # 蓝图内错误处理
bp.before_request # 蓝图内请求钩子
bp.app_context_processor # 模板全局变量
蓝图内钩子
api_bp = Blueprint("api", __name__, url_prefix="/api")
@api_bp.before_request
def before_api():
# 只对 /api 下的请求生效
token = request.headers.get("Authorization")
if not token:
return {"error": "未认证"}, 401
@api_bp.route("/data")
def data():
return {"ok": True}
蓝图错误处理
@api_bp.errorhandler(404)
def api_404(e):
return {"error": "API 不存在"}, 404
# 优先级:蓝图内 > 应用级
嵌套蓝图
# Flask 3.0+ 支持嵌套蓝图
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
users_bp = Blueprint("users", __name__, url_prefix="/users")
admin_bp.register_blueprint(users_bp)
app.register_blueprint(admin_bp)
# 完整路径:/admin/users/...
蓝图复用(可插拔)
# 同一个蓝图注册两次(不同前缀)
app.register_blueprint(api_bp, url_prefix="/v1")
app.register_blueprint(api_bp, url_prefix="/v2") # 需改名
# 蓝图带子域名
# bp = Blueprint("shop", __name__, subdomain="shop")
蓝图让模块"即插即用":写一次,可在多个应用/多个前缀下复用。大型 Flask 项目的组织基石。
Flask Request API
掌握 Flask Request API:request 对象的全部用法。
请求数据访问
from flask import request
# 基础信息
request.method # GET/POST/PUT/DELETE
request.path # /api/users
request.full_path # /api/users?page=2
request.url # 完整 URL
request.base_url # 不含查询串
request.host # localhost:5000
request.remote_addr # 客户端 IP
request.user_agent # User-Agent
request.is_json # 是否 JSON 请求
# 查询参数(GET)
request.args.get("page", default=1, type=int)
request.args.getlist("tag") # 多值 ?tag=a&tag=b
request.args.to_dict() # 全部转字典
# 表单(POST)
request.form.get("username")
request.form.getlist("tags")
# JSON
data = request.get_json(silent=True) # 解析失败返回 None
data = request.json # 简写
# 请求头
request.headers.get("Authorization")
request.headers.get("User-Agent")
# Cookie
request.cookies.get("theme")
# 文件
request.files.get("file")
# 原始数据
request.data # 字节
request.get_data() # 同上
完整示例
@app.route("/api/users", methods=["POST"])
def create_user():
# 支持 JSON 或表单
if request.is_json:
data = request.get_json()
else:
data = request.form
name = data.get("name")
if not name:
return {"error": "缺少 name"}, 400
page = request.args.get("page", 1, type=int)
user_agent = request.headers.get("User-Agent")
return {
"created": name,
"page": page,
"ua": user_agent[:50],
}, 201
request 是只读的全局对象(请求上下文内可用);参数解析给默认值和类型(type=int)可避免类型错误。
Flask Response API
掌握 Flask Response API:响应对象与返回方式。
返回方式汇总
from flask import Response, jsonify, redirect, make_response
# 1. 字符串
return "hello"
# 2. 字典(自动 JSON)
return {"ok": True, "data": [1, 2]}
# 3. 元组(内容, 状态码)
return "Not Found", 404
return {"error": "x"}, 400
# 4. 元组(内容, 状态码, 响应头)
return "内容", 200, {"X-Custom": "v"}
# 5. jsonify
return jsonify(ok=True, data=[1, 2])
# 6. 重定向
return redirect("/new-page")
# 7. Response 对象(最灵活)
resp = Response("内容", status=200, mimetype="text/plain")
return resp
make_response
# 任何返回值 → Response 对象
resp = make_response("内容")
resp.status_code = 404
resp.headers["X-Server"] = "Flask"
resp.set_cookie("k", "v", max_age=3600)
return resp
# 模板转 Response
resp = make_response(render_template("index.html"))
resp.headers["X-Page"] = "index"
return resp
Response 属性与方法
resp.status # "200 OK"
resp.status_code # 200
resp.headers # Headers 对象
resp.data # 字节内容
resp.mimetype # "text/html"
resp.content_type # "text/html; charset=utf-8"
resp.cookies # Cookie 操作
resp.set_cookie("name", "value", max_age=3600,
httponly=True, secure=True, samesite="Lax")
resp.delete_cookie("name")
自定义响应类
from flask import Response
class ApiResponse(Response):
default_mimetype = "application/json"
app.response_class = ApiResponse
# 或统一包装
def ok(data=None):
return jsonify({"code": 0, "data": data})
返回值三要素:内容、状态码、响应头。需要操作 Cookie/头时用 make_response 包装。
Flask Session API
掌握 Flask Session API:会话管理的核心用法。
Session 基础
from flask import session
# 需要 SECRET_KEY
app.secret_key = "your-secret"
# 写入
session["user_id"] = 1
session["username"] = "张三"
# 读取
user_id = session.get("user_id")
username = session.get("username", "游客")
# 判断存在
if "user_id" in session:
pass
# 删除
session.pop("username", None)
del session["user_id"] # 不存在会 KeyError
# 清空
session.clear()
会话配置
app.config.update(
SECRET_KEY="complex-secret",
SESSION_COOKIE_NAME="session", # Cookie 名
SESSION_COOKIE_HTTPONLY=True, # 防 XSS
SESSION_COOKIE_SECURE=False, # HTTPS 时开 True
SESSION_COOKIE_SAMESITE="Lax", # CSRF 防护
PERMANENT_SESSION_LIFETIME=86400, # 1 天
)
# 持久会话(关闭浏览器不丢)
session.permanent = True
完整登录示例
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
user = User.query.filter_by(
username=data.get("username")).first()
if not user or not check_password(user, data.get("password")):
return {"error": "用户名或密码错误"}, 401
session["user_id"] = user.id
session["username"] = user.username
session.permanent = True
return {"ok": True}
@app.route("/logout")
def logout():
session.clear()
return {"ok": True}
@app.route("/me")
def me():
uid = session.get("user_id")
if not uid:
return {"error": "未登录"}, 401
return {"user_id": uid, "username": session.get("username")}
装饰器保护
from functools import wraps
def login_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not session.get("user_id"):
return {"error": "请先登录"}, 401
return f(*args, **kwargs)
return wrapper
@app.get("/profile")
@login_required
def profile():
return {"ok": True}
默认 Session 存 Cookie(签名防篡改但不加密),别存敏感数据;生产建议 Flask-Session 换 Redis 存储。
Flask Session Interface API
掌握 Flask Session Interface API:自定义会话存储。
默认 Session 接口
# Flask 默认使用 SecureCookieSessionInterface
# Session 数据签名后存客户端 Cookie
from flask.sessions import SecureCookieSessionInterface
app.session_interface = SecureCookieSessionInterface()
自定义存储(Redis)
# 使用 Flask-Session 扩展(推荐)
from flask_session import Session
app.config.update(
SECRET_KEY="secret",
SESSION_TYPE="redis", # redis 存储
SESSION_REDIS=redis_client, # redis 连接
SESSION_PERMANENT=False,
SESSION_USE_SIGNER=True, # 签名
)
Session(app)
自定义 Session Interface
from flask.sessions import SessionInterface, SessionMixin
from flask import Flask, session
class MemorySession(dict, SessionMixin):
pass
class MemorySessionInterface(SessionInterface):
# 内存版 Session(演示)
sessions = {}
def open_session(self, app, request):
sid = request.cookies.get("sid")
if sid and sid in self.sessions:
return MemorySession(self.sessions[sid])
return MemorySession()
def save_session(self, app, session, response):
sid = request.cookies.get("sid", "default")
self.sessions[sid] = dict(session)
response.set_cookie("sid", sid)
app.session_interface = MemorySessionInterface()
接口方法
# SessionInterface 核心方法
class MyInterface(SessionInterface):
def open_session(self, app, request):
# 从请求恢复会话
pass
def save_session(self, app, session, response):
# 把会话保存到响应
pass
# 可选
def is_null_session(self, obj):
return False
def get_cookie_name(self, app):
return app.config["SESSION_COOKIE_NAME"]
日常开发用 Flask-Session(Redis/文件存储)即可;自定义 Interface 适合特殊存储需求(如对接已有系统)。
Flask Test Client API
掌握 Flask Test Client API:接口测试客户端。
创建测试客户端
import pytest
from app import create_app
@pytest.fixture
def client():
app = create_app("test")
app.config["TESTING"] = True
return app.test_client()
常用方法
# GET
resp = client.get("/api/users")
resp = client.get("/api/users?page=2&size=10")
# POST(JSON)
resp = client.post("/api/users",
json={"name": "张三"})
# POST(表单)
resp = client.post("/login",
data={"username": "a", "password": "b"})
# PUT / DELETE
resp = client.put("/api/users/1", json={"name": "李四"})
resp = client.delete("/api/users/1")
# 带请求头
resp = client.get("/api/me",
headers={"Authorization": "Bearer token"})
# 带 Cookie
client.set_cookie("session", "value")
resp = client.get("/me")
响应断言
def test_api(client):
resp = client.get("/api/users")
resp.status_code # 200
resp.status # "200 OK"
resp.headers["Content-Type"]
resp.data # 字节
resp.get_json() # 解析 JSON
resp.get_json()["data"]
# 文本内容
assert b"hello" in resp.data
assert resp.get_data(as_text=True).startswith("
会话保持
# 登录后测试受保护接口
def test_profile(client):
# 先登录(Session 自动保持)
client.post("/login", json={"username": "a", "password": "b"})
resp = client.get("/profile")
assert resp.status_code == 200
完整测试示例
def test_user_flow(client):
# 创建
resp = client.post("/api/users", json={"name": "张三"})
assert resp.status_code == 201
uid = resp.get_json()["id"]
# 查询
resp = client.get(f"/api/users/{uid}")
assert resp.get_json()["name"] == "张三"
# 更新
resp = client.put(f"/api/users/{uid}", json={"name": "李四"})
assert resp.get_json()["name"] == "李四"
# 删除
resp = client.delete(f"/api/users/{uid}")
assert resp.status_code == 200
# 404
resp = client.get(f"/api/users/{uid}")
assert resp.status_code == 404
Test Client 无需启动服务器即可模拟请求,是接口测试的标准工具;配合 pytest 断言状态码/数据。
Flask Test CLI Runner API
掌握 Flask Test CLI Runner API:命令行测试。
flask 命令入口
# 设置应用
export FLASK_APP=app.py
flask --help # 查看所有命令
# 常用命令
flask run # 启动开发服务器
flask routes # 列出所有路由
flask shell # 交互式 shell(带上下文)
自定义 CLI 命令
import click
from flask import Flask
app = Flask(__name__)
@app.cli.command("init-db")
def init_db_command():
# 初始化数据库
db.create_all()
click.echo("数据库已初始化")
@app.cli.command("create-admin")
@click.argument("username")
@click.option("--password", prompt=True, hide_input=True)
def create_admin(username, password):
# 创建管理员
admin = User(username=username, password=password, is_admin=True)
db.session.add(admin)
db.session.commit()
click.echo(f"管理员 {username} 已创建")
flask init-db
flask create-admin admin_user
flask create-admin --password 123456 bob
测试 CLI 命令
from click.testing import CliRunner
def test_cli():
runner = CliRunner()
result = runner.invoke(app.cli, ["init-db"])
assert result.exit_code == 0
assert "初始化" in result.output
cli 注册方式
# 方式一:装饰器(上面示例)
# 方式二:单独函数
import click
@click.command("init-db")
def init_db():
pass
app.cli.add_command(init_db)
# 方式三:分组
@app.cli.group()
def db():
# 数据库命令组
pass
@db.command("init")
def db_init():
click.echo("db init")
# flask db init
把"初始化、建管理员、数据清理"等运维操作做成 CLI 命令,比手动执行代码可靠得多。
Flask Application Globals
掌握 Flask Application Globals:g 与 current_app。
g 对象(请求级全局)
from flask import g
# g 在同一个请求内跨函数共享
@app.before_request
def load_user():
# 每次请求前加载用户
g.user = User.query.first()
@app.route("/me")
def me():
# 视图内直接使用
return {"user": g.user.name if g.user else None}
# 多函数共享
def get_current_user():
# 缓存到 g,避免重复查询
if "user" not in g:
g.user = User.query.first()
return g.user
g 的应用场景
# 1. 数据库连接
@app.before_request
def open_db():
g.db = connect_db()
@app.teardown_request
def close_db(exc):
g.db.close()
# 2. 请求日志/耗时
@app.before_request
def start_timer():
g.start = time.time()
# 3. 配置缓存
def get_settings():
if "settings" not in g:
g.settings = load_settings()
return g.settings
current_app
from flask import current_app
# 在请求/应用上下文中访问应用配置
current_app.config["SECRET_KEY"]
current_app.name # 应用名
current_app.logger # 应用日志
# 在扩展初始化中常用
def init_db(app):
with app.app_context():
db.init_app(app)
g 的注意事项
- g 只在一个请求内有效,请求结束清空
- 不能跨请求/跨线程使用
- 适合缓存请求内临时数据
- 不要在 g 存大对象
g = "请求内的全局变量";current_app = "应用全局"。两者配合 before/teardown 钩子实现连接管理等横切逻辑。
Flask 实用函数与类
掌握 Flask 实用函数与类:常用内置工具。
URL 工具
from flask import url_for, request
# 反向生成 URL
url_for("index")
url_for("user", uid=42)
url_for("static", filename="css/style.css")
url_for("api.users", page=2) # /api/users?page=2
# 当前 URL
request.url
request.base_url
request.host_url
安全工具
from werkzeug.security import generate_password_hash, check_password_hash
# 密码哈希(绝不存明文)
hashed = generate_password_hash("mypassword")
check_password_hash(hashed, "mypassword") # True
from werkzeug.utils import secure_filename
# 安全文件名(防路径穿越)
secure_filename("../../etc/passwd") # "etc_passwd"
JSON 工具
from flask import jsonify
import json
# Flask 的 jsonify(带 MIME)
jsonify({"ok": True})
# 标准 json
json.dumps({"a": 1}, ensure_ascii=False)
json.loads('{"a": 1}')
# 序列化对象
json.dumps(obj, default=str) # 处理 datetime 等
请求/响应工具
from flask import abort, redirect, make_response
abort(404) # 终止并返回错误
abort(403, "无权访问")
redirect("/") # 重定向
make_response("内容") # 构造响应
from flask import send_file, send_from_directory
send_file("file.pdf", as_attachment=True)
send_from_directory("uploads", "a.pdf")
模板全局工具
# 模板可用的内置对象
config # 应用配置
request # 请求
session # 会话
g # 全局
url_for() # URL 生成
get_flashed_messages() # 消息
密码哈希用 Werkzeug 内置(无需装 bcrypt);secure_filename 是上传文件的安全必备。
Flask Message Flashing API
掌握 Flask Message Flashing API:闪现消息。
基本用法
from flask import flash, redirect, render_template
# 视图里发送消息
@app.route("/submit", methods=["POST"])
def submit():
# 业务处理...
flash("保存成功!", "success") # 消息 + 类别
flash("记得备份", "warning")
return redirect("/")
# 一次性:下次请求读取后消失
模板中显示
<!-- base.html -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="messages">
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
按类别过滤
# 只取某类别
get_flashed_messages(category_filter=["error"])
# 模板中
{% for msg in get_flashed_messages(category_filter=["error"]) %}
<p class="error">{{ msg }}</p>
{% endfor %}
API 场景替代
# 前后端分离项目,flash 不太适用
# 直接返回结构化信息
return jsonify({
"success": True,
"message": "保存成功",
"code": 0,
}), 200
# flash 适合服务端渲染(传统 Web)场景
完整示例
@app.route("/register", methods=["POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
user = User(username=form.username.data)
db.session.add(user)
db.session.commit()
flash(f"欢迎加入,{user.username}!", "success")
return redirect(url_for("index"))
flash("表单校验失败,请检查", "error")
return render_template("register.html", form=form)
flash 适合"PRG 模式"(Post-Redirect-Get):提交后重定向,消息在目标页显示一次。API 项目用 JSON 返回消息。
Flask JSON
掌握 Flask JSON:JSON 的序列化与解析。
返回 JSON
from flask import jsonify
# 字典自动转 JSON
@app.route("/api/data")
def data():
return jsonify({
"name": "CodeNest",
"version": 1.0,
"tags": ["教程", "编程"],
"active": True
})
# 数组
return jsonify([1, 2, 3])
# 指定状态码
return jsonify({"error": "未找到"}), 404
序列化对象
# 模型转字典
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
created_at = db.Column(db.DateTime)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
# 列表
users = User.query.all()
return jsonify([u.to_dict() for u in users])
解析 JSON 请求
from flask import request
@app.post("/api/users")
def create():
data = request.get_json() # 解析失败返回 None
if data is None:
return {"error": "无效的 JSON"}, 400
# silent=True:解析失败不抛异常
data = request.get_json(silent=True)
# force=True:非 JSON Content-Type 也尝试解析
data = request.get_json(force=True)
name = data.get("name")
return {"created": name}, 201
JSON 编码细节
# 中文不转义
import json
json.dumps({"name": "张三"}, ensure_ascii=False)
# '{"name": "张三"}'
# 处理不可序列化对象
json.dumps({"date": datetime.now()}, default=str)
# Flask 配置
app.config["JSON_AS_ASCII"] = False # 中文原样(Flask 2.2 前)
# Flask 3.x 默认 ensure_ascii=False
统一响应包装
def api_response(data=None, message="ok", code=0, status=200):
return jsonify({
"code": code,
"message": message,
"data": data,
}), status
@app.get("/api/users")
def users():
return api_response([{"id": 1}])
@app.post("/api/users")
def create_user():
data = request.get_json()
if not data or not data.get("name"):
return api_response(message="参数错误", code=400, status=400)
return api_response({"id": 1}, status=201)
前后端分离项目统一 JSON 响应格式(code/message/data),前端只需处理一套结构。
Flask 模板渲染 API
掌握 Flask 模板渲染 API:Jinja2 高级用法。
render_template 参数
from flask import render_template
# 基本
render_template("index.html", title="首页", users=users)
# 模板字符串(不常用)
from flask import render_template_string
render_template_string("<h1>{{ title }}</h1>", title="Hello")
模板上下文处理器
# 给所有模板注入变量
@app.context_processor
def inject_globals():
return {
"site_name": "CodeNest",
"current_year": datetime.now().year,
}
# 所有模板可直接使用 {{ site_name }}
# <p>{{ site_name }} © {{ current_year }}</p>
模板过滤器
# 自定义过滤器
@app.template_filter("money")
def money_filter(value):
return f"¥{value:,.2f}"
# 模板中
# {{ price | money }} → ¥1,234.56
# 注册函数到模板
@app.template_global()
def format_time(dt):
return dt.strftime("%Y-%m-%d %H:%M")
Jinja2 高级语法
<!-- 宏(可复用片段)-->
{% macro input(name, type="text", placeholder="") %}
<input type="{{ type }}" name="{{ name }}" placeholder="{{ placeholder }}">
{% endmacro %}
{{ input("username") }}
{{ input("password", type="password") }}
<!-- 循环高级 -->
{% for item in items %}
{% if loop.first %}首个{% endif %}
{{ loop.index }}. {{ item.name }} <!-- 索引从 1 开始 -->
{% if loop.last %}末个{% endif %}
{% endfor %}
<!-- set 定义变量 -->
{% set base_price = 100 %}
<p>{{ base_price * 0.9 }}</p>
转义与安全
<!-- 自动转义(防 XSS)-->
{{ user_input }} <!-- <script> 会被转义 -->
<!-- 确需输出 HTML(仅可信内容)-->
{{ content | safe }}
<!-- 禁用全局自动转义(不推荐)-->
{% autoescape false %}
{{ content }}
{% endautoescape %}
模板保持"薄":复杂逻辑放视图/过滤器;| safe 只用于可信 HTML;上下文处理器注入公共变量。
Flask Config API
掌握 Flask Config API:配置加载与访问。
配置属性访问
from flask import Flask, current_app
app = Flask(__name__)
# 设置
app.config["DEBUG"] = True
app.config["SECRET_KEY"] = "secret"
# 读取(在上下文中)
current_app.config["SECRET_KEY"]
current_app.config.get("NOT_EXIST", "default")
# 视图外(无上下文)
with app.app_context():
val = current_app.config["DEBUG"]
常用配置项
app.config.update(
DEBUG=False, # 调试
SECRET_KEY="complex", # 密钥
JSON_AS_ASCII=False, # JSON 中文
SESSION_COOKIE_HTTPONLY=True, # Session 安全
SQLALCHEMY_DATABASE_URI="sqlite:///app.db",
MAX_CONTENT_LENGTH=16 * 1024 * 1024, # 上传限制 16MB
PERMANENT_SESSION_LIFETIME=86400,
)
配置加载方式
# from_object
app.config.from_object("config.ProdConfig")
# from_pyfile
app.config.from_pyfile("config.py")
# from_envvar(环境变量指向配置文件)
app.config.from_envvar("APP_CONFIG")
# from_mapping
app.config.from_mapping(
DEBUG=True,
SECRET_KEY="x",
)
环境变量配置
import os
app.config.update(
SECRET_KEY=os.environ.get("SECRET_KEY", "dev-secret"),
DB_URL=os.environ.get("DB_URL", "sqlite:///dev.db"),
)
读取配置最佳实践
# 在扩展初始化中使用
db.init_app(app) # 内部读取 SQLALCHEMY_DATABASE_URI
# 自定义配置封装
def get_config(key, default=None):
return current_app.config.get(key, default)
配置加载顺序建议:默认值 → 配置文件 → 环境变量覆盖。密钥绝不硬编码。
Flask Stream Helpers API
掌握 Flask Stream Helpers API:流式响应。
流式响应基础
from flask import Response
@app.route("/stream")
def stream():
def generate():
for i in range(10):
yield f"数据块 {i}
"
return Response(generate(), mimetype="text/plain")
# 客户端逐步接收,而非一次性
实时日志流
import time
@app.route("/logs")
def stream_logs():
def generate():
while True:
yield f"日志时间: {time.time()}
"
time.sleep(1)
return Response(generate(), mimetype="text/plain")
大文件下载
# 大文件分块发送(省内存)
@app.route("/download")
def download():
def generate():
with open("big_file.zip", "rb") as f:
while chunk := f.read(8192):
yield chunk
return Response(
generate(),
headers={"Content-Disposition": "attachment; filename=big.zip"},
mimetype="application/zip",
)
SSE(Server-Sent Events)
import json
@app.route("/events")
def sse():
def generate():
yield "retry: 3000
" # 3 秒重连
for i in range(5):
data = json.dumps({"count": i})
yield f"data: {data}
" # SSE 格式
time.sleep(1)
return Response(generate(), mimetype="text/event-stream")
// 前端接收 SSE
const es = new EventSource("/events");
es.onmessage = (e) => console.log(JSON.parse(e.data));
注意
- 流式响应用生成器(yield)
- 长时间流需考虑代理超时设置
- SSE 是单向推送(服务器→客户端);双向用 WebSocket
流式响应适用:大文件传输、实时日志、SSE 推送。yield 生成器实现,简单高效。
Flask 内部对象 API
掌握 Flask 内部对象 API:请求/应用上下文内部机制。
本地代理(Local Proxy)
# request/session/current_app/g 都是"本地代理"
# 它们根据当前上下文自动指向正确的对象
from flask import request, current_app, g
# 使用(无需关心上下文切换)
request.method
current_app.name
g.user
上下文对象
from flask import Flask, request, session
app = Flask(__name__)
# 请求上下文:request + session
# 应用上下文:current_app + g
# 手动管理(一般用 with)
with app.test_request_context("/?page=2"):
print(request.args.get("page")) # 2
# 内部实现
from flask.ctx import RequestContext, AppContext
# 上下文栈(ctx stack)
# flask._app_ctx_stack
# flask._request_ctx_stack
current_app 工作原理
# current_app 是 LocalProxy
# 它从应用上下文栈取栈顶的 app
# 为什么要代理?
# - 一个进程可能有多个 Flask 应用(测试时常见)
# - 代理保证总是取"当前"的应用
# 直接使用 app 对象也可以(简单场景)
app.config["DEBUG"]
g 的生命周期
# g 绑定在应用上下文
# 应用上下文 push → g 可用
# 应用上下文 pop → g 清空
with app.app_context():
g.x = 1
# 离开 with 后 g 不可用
# 每个请求:请求上下文 push → 视图 → pop
# 所以 g 是"请求级"的
何时需要了解内部机制
- 调试"working outside of application context"错误
- 编写扩展(需要访问上下文)
- 理解测试中的 context
- 多应用/多线程场景
日常开发只需知道:请求内用 request/session/g,上下文外用 current_app/db;报 "outside context" 时用 with app.app_context()。
Flask 信号 API
掌握 Flask 信号 API:事件通知机制(Blinker)。
信号基础
# Flask 内置信号(需要 blinker)
from flask.signals import (
request_started, request_finished,
template_rendered, got_request_exception
)
# 订阅信号
def log_request(sender, **extra):
print("请求开始")
request_started.connect(log_request)
# 可取消订阅
request_started.disconnect(log_request)
内置信号
| 信号 | 触发时机 |
|---|---|
| request_started | 请求开始时 |
| request_finished | 响应发送前 |
| got_request_exception | 请求处理抛异常时 |
| template_rendered | 模板渲染后 |
| before_render_template | 模板渲染前 |
| message_flashed | flash 消息时 |
| appcontext_pushed/popped | 应用上下文压栈/出栈 |
自定义信号
from blinker import Namespace
# 创建命名空间
my_signals = Namespace()
# 定义信号
user_registered = my_signals.signal("user-registered")
user_logged_in = my_signals.signal("user-logged-in")
# 发送信号
@app.route("/register", methods=["POST"])
def register():
user = create_user(...)
user_registered.send(app, user=user) # 发送(携带数据)
return {"ok": True}
订阅自定义信号
def send_welcome_email(sender, user, **extra):
print(f"给 {user.username} 发欢迎邮件")
user_registered.connect(send_welcome_email)
# 装饰器方式
@user_registered.connect
def log_register(sender, user, **extra):
app.logger.info(f"新用户注册: {user.username}")
信号 vs 钩子
| 对比项 | 信号 | 钩子(before_request) |
|---|---|---|
| 解耦 | 发送方不关心订阅者 | 注册在应用上 |
| 灵活 | 任意模块订阅 | 固定生命周期 |
| 适用 | 业务事件(注册/登录) | 请求前后处理 |
信号用于业务事件解耦(注册后发邮件/记日志,无需侵入注册逻辑);需安装 blinker。
Flask 类视图 API
掌握 Flask 类视图 API:用类组织视图(Flask 3.0+)。
method_view(Flask 3.0+)
from flask import request, jsonify
from flask.views import MethodView
# 类视图:按 HTTP 方法组织
class UserView(MethodView):
def get(self):
users = User.query.all()
return jsonify([u.to_dict() for u in users])
def post(self):
data = request.get_json()
user = User(name=data["name"])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
class UserDetailView(MethodView):
def get(self, uid):
user = db.session.get(User, uid)
if not user:
return {"error": "不存在"}, 404
return jsonify(user.to_dict())
def put(self, uid):
user = db.session.get(User, uid)
if not user:
return {"error": "不存在"}, 404
data = request.get_json()
user.name = data.get("name", user.name)
db.session.commit()
return jsonify(user.to_dict())
def delete(self, uid):
user = db.session.get(User, uid)
if not user:
return {"error": "不存在"}, 404
db.session.delete(user)
db.session.commit()
return {"ok": True}
# 注册
app.add_url_rule("/api/users", view_func=UserView.as_view("users"))
app.add_url_rule("/api/users/",
view_func=UserDetailView.as_view("user_detail"))
类视图装饰器
class AdminView(MethodView):
decorators = [login_required] # 所有方法加装饰器
def get(self):
return {"admin": True}
def post(self):
return {"created": True}
类视图 vs 函数视图
| 对比项 | 类视图 | 函数视图 |
|---|---|---|
| 方法组织 | get/post/put 分离 | if method 判断 |
| 复用 | 继承、装饰器列表 | 装饰器 |
| 清晰度 | CRUD 更清晰 | 简单场景更直接 |
| 适用 | REST API、复杂视图 | 简单页面/接口 |
RESTful CRUD 接口用 MethodView 组织更清晰;简单接口用函数视图更轻量。蓝图 + MethodView 是 API 项目标配。
Flask 定义路由 API
掌握 Flask 定义路由 API:路由注册的多种方式。
方式一:装饰器(最常用)
@app.route("/")
def index():
return "首页"
# 指定方法
@app.route("/data", methods=["GET", "POST"])
def data():
return {"ok": True}
# 简化写法(Flask 2.0+)
@app.get("/data")
def get_data(): ...
@app.post("/data")
def post_data(): ...
@app.put("/data")
def put_data(): ...
@app.delete("/data")
def delete_data(): ...
方式二:add_url_rule
def index():
return "首页"
app.add_url_rule("/", "index", index)
# 带方法
app.add_url_rule("/api/users", "users", users_view,
methods=["GET", "POST"])
# 等价于 @app.route
方式三:蓝图注册
bp = Blueprint("api", __name__, url_prefix="/api")
@bp.route("/users")
def users():
return {"ok": True}
app.register_blueprint(bp)
方式四:类视图
from flask.views import MethodView
class UsersView(MethodView):
def get(self): return {"method": "get"}
def post(self): return {"method": "post"}
app.add_url_rule("/api/users", view_func=UsersView.as_view("users"))
路由命名与 url_for
# 默认端点名 = 函数名
@app.route("/user/")
def user_detail(uid): ...
url_for("user_detail", uid=42) # /user/42
# 自定义端点名
@app.route("/x", endpoint="my_x")
def x(): ...
url_for("my_x")
查看所有路由
flask routes
# 或
# python -c "from app import app; [print(r) for r in app.url_map]"
# Endpoint Methods Rule
# index GET /
# users GET,POST /api/users
四种方式本质相同:装饰器最简单、add_url_rule 最底层、蓝图做模块化、MethodView 做 REST。
Flask 视图函数 API
掌握 Flask 视图函数 API:返回值与请求处理。
视图返回类型
from flask import jsonify, render_template, redirect, Response
# 1. 字符串(默认 text/html)
return "hello"
# 2. 字典/列表(自动 JSON)
return {"ok": True}
return [1, 2, 3]
# 3. 元组
return "内容", 201
return "内容", 200, {"X-Custom": "v"}
# 4. jsonify
return jsonify(ok=True)
# 5. 模板
return render_template("index.html")
# 6. 重定向
return redirect("/other")
# 7. Response
return Response("内容", status=200)
获取请求数据
from flask import request
@app.post("/api/users")
def create():
# 参数优先级:args(URL) > form > json > files
data = request.get_json() or request.form
name = data.get("name")
page = request.args.get("page", 1, type=int)
# 校验
if not name:
return {"error": "缺少 name"}, 400
return {"created": name, "page": page}, 201
错误返回
from flask import abort
# 快速错误
abort(404)
abort(400, "参数错误") # 自定义描述
# 自定义错误处理
@app.errorhandler(400)
def bad_request(e):
return jsonify({
"error": "Bad Request",
"detail": getattr(e, "description", "")
}), 400
请求钩子与视图
@app.before_request
def before():
# 视图前的统一处理
pass
@app.after_request
def after(response):
# 视图后的统一处理
return response
视图最佳实践
- 参数解析(request)与校验分离
- 业务逻辑抽到 service 层
- 统一返回格式(code/message/data)
- 视图保持"薄"(薄视图,厚服务)
薄视图原则:视图只做"取参数 → 调服务 → 返响应",逻辑放 service/models,测试与维护都轻松。
Flask 命令行 API
掌握 Flask 命令行 API:CLI 命令与交互式 shell。
flask 命令基础
export FLASK_APP=app.py
export FLASK_ENV=development # 开发模式(2.3 前)
# Flask 2.3+ 用 --debug
flask --app app.py run --debug
# 常用命令
flask run # 启动开发服务器
flask routes # 列出路由
flask shell # 交互式 shell
flask --help # 帮助
自定义命令
import click
from flask import Flask
app = Flask(__name__)
@app.cli.command("init-db")
def init_db():
# 初始化数据库
db.create_all()
click.echo("数据库初始化完成")
@app.cli.command("seed")
@click.option("--count", default=10, help="数量")
def seed(count):
# 填充测试数据
for i in range(count):
db.session.add(User(name=f"用户{i}"))
db.session.commit()
click.echo(f"已创建 {count} 条数据")
flask init-db
flask seed --count 20
命令分组
@app.cli.group("db")
def db_group():
# 数据库命令组
@db_group.command("init")
def db_init():
click.echo("db init")
@db_group.command("drop")
def db_drop():
click.echo("db drop")
# flask db init / flask db drop
flask shell 增强
# 自动导入常用对象到 shell
@app.shell_context_processor
def make_shell_context():
return {
"db": db,
"User": User,
"app": app,
}
# flask shell 后可直接用 db、User
CLI 测试
from click.testing import CliRunner
def test_cli():
runner = CliRunner()
result = runner.invoke(app.cli, ["init-db"])
assert result.exit_code == 0
把运维操作(初始化、种子数据、清理)做成 CLI 命令,比手动跑脚本可靠;flask shell 适合调试。
Flask Web 应用
综合运用 Flask 知识,构建完整 Web 应用。
项目规划
功能:
1. 文章列表(分页 + 搜索)
2. 文章详情
3. 分类筛选
4. 用户注册/登录
5. 后台管理(增删改)
6. 部署上线
项目结构
blog/
├── run.py # 入口
├── config.py # 配置
├── app/
│ ├── __init__.py # 工厂
│ ├── models.py # 模型
│ ├── forms.py # 表单
│ ├── views/
│ │ ├── __init__.py
│ │ ├── main.py # 前台
│ │ ├── auth.py # 认证
│ │ └── admin.py # 后台
│ ├── templates/
│ └── static/
├── requirements.txt
└── .env
工厂函数
# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config.from_object("config.Config")
db.init_app(app)
# 注册蓝图
from .views.main import main_bp
from .views.auth import auth_bp
from .views.admin import admin_bp
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(admin_bp, url_prefix="/admin")
return app
模型
# app/models.py
from . import db
from datetime import datetime
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True)
articles = db.relationship("Article", backref="category")
class Article(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text)
category_id = db.Column(db.Integer,
db.ForeignKey("category.id"))
views = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
前台视图
# app/views/main.py
from flask import Blueprint, render_template
from ..models import Article
main_bp = Blueprint("main", __name__)
@main_bp.route("/")
def index():
page = request.args.get("page", 1, type=int)
articles = Article.query.order_by(
Article.created_at.desc()).paginate(
page=page, per_page=10)
return render_template("index.html", articles=articles)
后台管理
# app/views/admin.py
from flask import Blueprint, request, redirect, url_for
from ..models import Article, db
admin_bp = Blueprint("admin", __name__)
@admin_bp.route("/articles/create", methods=["GET", "POST"])
def create():
if request.method == "POST":
article = Article(
title=request.form["title"],
content=request.form["content"],
)
db.session.add(article)
db.session.commit()
return redirect(url_for("main.index"))
return render_template("admin/form.html")
完整项目 = 工厂模式 + 蓝图 + 模型 + 模板 + 表单。对照本站 CodeNest(FastAPI)的结构,思路相通。
Jinja2 模板
掌握 Jinja2 模板:Flask 内置模板引擎详解。
模板基础
<!-- 变量与过滤器 -->
<p>{{ user.name | upper }}</p>
<p>{{ content | truncate(30) }}</p>
<p>{{ price | round(2) }}</p>
<!-- 控制结构 -->
{% if user.is_admin %}
<a href="/admin">后台</a>
{% endif %}
{% for item in items %}
<li>{{ loop.index }}. {{ item }}</li>
{% endfor %}
继承与块
<!-- base.html -->
<html>
<head>
<title>{% block title %}CodeNest{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
{% include "nav.html" %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}
</body>
</html>
自定义过滤器
@app.template_filter("timeago")
def timeago(dt):
seconds = (datetime.now() - dt).total_seconds()
if seconds < 60: return "刚刚"
if seconds < 3600: return f"{int(seconds//60)} 分钟前"
if seconds < 86400: return f"{int(seconds//3600)} 小时前"
return f"{int(seconds//86400)} 天前"
# 模板:{{ article.created_at | timeago }}
模板测试
{% if user is defined %}已定义{% endif %}
{% if items is iterable %}可迭代{% endif %}
{% if 'a' in items %}包含{% endif %}
性能与安全
- 默认自动转义(防 XSS)
- 复杂逻辑放视图/过滤器,模板保持简单
- 频繁渲染可开模板缓存(app.config["TEMPLATES_AUTO_RELOAD"])
Jinja2 三要素:变量 {{ }}、语句 {% %}、注释 {# #}。继承 + 宏(macro)实现模板复用。
Flask-SQLAlchemy
掌握 Flask-SQLAlchemy:Flask 与 SQLAlchemy 的集成。
初始化
pip install flask-sqlalchemy
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
定义模型
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(100), unique=True)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 关系
articles = db.relationship("Article", backref="author",
lazy="dynamic")
def to_dict(self):
return {
"id": self.id,
"username": self.username,
"email": self.email,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class Article(db.Model):
__tablename__ = "articles"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
category_id = db.Column(db.Integer, db.ForeignKey("categories.id"))
views = db.Column(db.Integer, default=0)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True)
articles = db.relationship("Article", backref="category")
建表与增删改查
# 建表
with app.app_context():
db.create_all()
# 增
user = User(username="张三", email="z@example.com")
db.session.add(user)
db.session.commit()
# 查
User.query.all() # 全部
User.query.get(1) # 按主键
User.query.filter_by(username="张三").first()
User.query.filter(User.id > 5).all()
User.query.order_by(User.created_at.desc()).limit(10).all()
# 改
user = User.query.get(1)
user.username = "李四"
db.session.commit()
# 删
db.session.delete(user)
db.session.commit()
分页
page_obj = Article.query.paginate(page=1, per_page=10)
page_obj.items # 当前页数据
page_obj.total # 总数
page_obj.pages # 总页数
page_obj.has_next # 是否有下一页
page_obj.next_num # 下一页页码
Flask-SQLAlchemy 让数据库操作变成 Python 对象操作;本站 CodeNest 的数据库结构(categories/tutorials/sections)正是 ORM 模型。
视图函数
掌握 Flask 视图函数:视图的组织与模式。
视图模式
# 1. 简单视图
@app.route("/")
def index():
return render_template("index.html")
# 2. 带参数
@app.route("/article/")
def article(aid):
a = Article.query.get_or_404(aid)
return render_template("detail.html", article=a)
# 3. 处理表单
@app.route("/create", methods=["GET", "POST"])
def create():
if request.method == "POST":
title = request.form.get("title")
if not title:
flash("标题必填", "error")
else:
db.session.add(Article(title=title))
db.session.commit()
flash("创建成功", "success")
return redirect(url_for("index"))
return render_template("create.html")
装饰器封装
from functools import wraps
from flask import redirect, url_for, session
def login_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not session.get("user_id"):
flash("请先登录", "warning")
return redirect(url_for("auth.login"))
return f(*args, **kwargs)
return wrapper
def admin_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not session.get("is_admin"):
abort(403)
return f(*args, **kwargs)
return wrapper
@app.route("/dashboard")
@login_required
def dashboard():
return render_template("dashboard.html")
@app.route("/admin")
@login_required
@admin_required
def admin():
return render_template("admin.html")
错误视图
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404
@app.errorhandler(403)
def forbidden(e):
return render_template("403.html"), 403
视图组织建议
- 按蓝图分模块(main/auth/admin)
- 权限用装饰器统一管理
- 业务逻辑抽 service 层
- 错误页统一处理
视图层三件套:装饰器(权限)、flash(消息)、redirect/url_for(跳转),配合蓝图组织模块。
Flask-Migrate
掌握 Flask-Migrate:数据库迁移(版本控制表结构)。
安装与初始化
pip install 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:///app.db"
db = SQLAlchemy(app)
migrate = Migrate(app, db)
# 模型...
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
export FLASK_APP=app.py
# 初始化迁移目录
flask db init
# 生成迁移脚本(对比模型与数据库)
flask db migrate -m "create users table"
# 应用迁移
flask db upgrade
# 回滚一个版本
flask db downgrade
# 查看状态
flask db current
flask db history
模型变更流程
1. 修改 models.py(如加字段)
2. flask db migrate -m "add email to users"
3. 检查生成的迁移文件
4. flask db upgrade
5. 测试
迁移文件
# migrations/versions/xxx_create_users.py
def upgrade():
op.add_column("users",
sa.Column("email", sa.String(100), nullable=True))
def downgrade():
op.drop_column("users", "email")
为什么需要迁移
- 表结构变更可追踪、可回滚
- 团队协作同步表结构
- 生产环境安全升级
- 替代手动 ALTER TABLE
生产环境表结构变更务必用迁移:migrate 生成 → 检查 → upgrade 应用。别在生产库手动 ALTER。
关键词搜索与分页
掌握 Flask 关键词搜索与分页:内容站核心功能。
搜索实现
@app.route("/search")
def search():
keyword = request.args.get("q", "").strip()
if not keyword:
return render_template("search.html", articles=[])
# LIKE 模糊搜索
articles = Article.query.filter(
Article.title.contains(keyword) |
Article.content.contains(keyword)
).order_by(Article.created_at.desc()).all()
return render_template("search.html",
articles=articles, keyword=keyword)
分页实现
@app.route("/")
def index():
page = request.args.get("page", 1, type=int)
per_page = 10
# 分页查询
pagination = Article.query.order_by(
Article.created_at.desc()
).paginate(page=page, per_page=per_page)
return render_template("index.html", pagination=pagination)
分页模板
<!-- 分页导航 -->
{% if pagination.pages > 1 %}
<nav class="pagination">
{% if pagination.has_prev %}
<a href="?page={{ pagination.prev_num }}">上一页</a>
{% endif %}
<span>{{ pagination.page }} / {{ pagination.pages }}</span>
{% if pagination.has_next %}
<a href="?page={{ pagination.next_num }}">下一页</a>
{% endif %}
</nav>
{% endif %}
搜索 + 分页组合
@app.route("/search")
def search():
keyword = request.args.get("q", "").strip()
page = request.args.get("page", 1, type=int)
query = Article.query
if keyword:
query = query.filter(
Article.title.contains(keyword))
pagination = query.order_by(
Article.created_at.desc()
).paginate(page=page, per_page=10)
return render_template("search.html",
pagination=pagination,
keyword=keyword)
<!-- 分页时保留搜索词 -->
<a href="?q={{ keyword }}&page={{ pagination.next_num }}">下一页</a>
优化建议
- 数据量大时 LIKE 慢,用全文索引(MySQL FULLTEXT)
- 更大规模用 Elasticsearch
- 分页加 ORDER BY(稳定顺序)
- 关键词防注入:SQLAlchemy 自动参数化
搜索+分页是内容站标配:filter(条件)+ order_by(排序)+ paginate(分页)三段式。
Flask-Login
掌握 Flask-Login:用户认证扩展。
安装与初始化
pip install flask-login
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "auth.login" # 未登录跳转
login_manager.login_message = "请先登录" # 提示
# User 模型继承 UserMixin
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True)
password_hash = db.Column(db.String(128))
# 用户加载器
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
登录/登出
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
user = User.query.filter_by(
username=request.form["username"]).first()
if user and check_password(user.password_hash,
request.form["password"]):
login_user(user, remember=True) # 记住登录
return redirect(request.args.get("next") or url_for("index"))
flash("用户名或密码错误", "error")
return render_template("login.html")
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("index"))
保护视图
from flask_login import login_required, current_user
@app.route("/profile")
@login_required
def profile():
return render_template("profile.html", user=current_user)
# 模板中判断
# {% if current_user.is_authenticated %}已登录{% endif %}
# {% if current_user.is_authenticated %}
# {{ current_user.username }}
# {% else %}
# <a href="{{ url_for('login') }}">登录</a>
# {% endif %}
权限控制
from functools import wraps
def admin_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user.is_admin:
abort(403)
return f(*args, **kwargs)
return wrapper
@app.route("/admin")
@login_required
@admin_required
def admin():
return "管理后台"
其他功能
# 未认证访问 → 跳转登录页(login_view)
# remember=True → 关闭浏览器保持登录
# current_user → 模板/视图中的当前用户
# is_authenticated → 是否登录
# is_anonymous → 是否匿名
Flask-Login 是认证的标准方案:UserMixin 模型 + user_loader + login_user/logout_user + login_required。
表单处理
掌握 Flask 表单处理:Flask-WTF 完整实践。
安装
pip install flask-wtf
定义表单
from flask_wtf import FlaskForm
from wtforms import (StringField, PasswordField, TextAreaField,
BooleanField, SelectField, DateField)
from wtforms.validators import (DataRequired, Email, Length,
EqualTo, Optional)
class ArticleForm(FlaskForm):
title = StringField("标题", validators=[
DataRequired(message="标题必填"),
Length(min=3, max=100, message="3-100 字符"),
])
category = SelectField("分类", coerce=int, validators=[
DataRequired()])
content = TextAreaField("内容", validators=[
DataRequired()])
is_published = BooleanField("发布")
class RegisterForm(FlaskForm):
username = StringField("用户名", validators=[
DataRequired(), Length(3, 20)])
email = StringField("邮箱", validators=[
DataRequired(), Email()])
password = PasswordField("密码", validators=[
DataRequired(), Length(6, 50)])
confirm = PasswordField("确认密码", validators=[
EqualTo("password", message="两次密码不一致")])
视图处理
@app.route("/articles/new", methods=["GET", "POST"])
def new_article():
form = ArticleForm()
form.category.choices = [
(c.id, c.name) for c in Category.query.all()]
if form.validate_on_submit(): # POST + 校验通过
article = Article(
title=form.title.data,
content=form.content.data,
category_id=form.category.data,
is_published=form.is_published.data,
)
db.session.add(article)
db.session.commit()
flash("文章已发布", "success")
return redirect(url_for("index"))
return render_template("form.html", form=form)
模板渲染
<form method="post">
{{ form.hidden_tag() }} <!-- CSRF 令牌(必须) -->
<div>
{{ form.title.label }}
{{ form.title(class="form-control") }}
{% for err in form.title.errors %}
<span class="error">{{ err }}</span>
{% endfor %}
</div>
<div>{{ form.category.label }} {{ form.category() }}</div>
<div>{{ form.content.label }} {{ form.content(rows=8) }}</div>
<div>{{ form.is_published() }} {{ form.is_published.label }}</div>
<button type="submit">保存</button>
</form>
错误处理
@app.route("/register", methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
# 处理注册...
flash("注册成功", "success")
return redirect(url_for("login"))
# 校验失败:form.errors 含错误信息
# 模板中自动显示(errors 循环)
return render_template("register.html", form=form)
CSRF 说明
# Flask-WTF 默认开启 CSRF
# 必须配置 SECRET_KEY
# 模板必须 {{ form.hidden_tag() }}
# 或单独 token:<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
# API 场景可关闭
# app.config["WTF_CSRF_ENABLED"] = False
表单三件套:FlaskForm 定义 + validators 校验 + 模板渲染。CSRF 保护默认开启,务必保留。
收藏功能与后台管理
掌握 Flask 收藏功能与后台管理:完整业务功能。
收藏模型
# models.py
class Favorite(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
article_id = db.Column(db.Integer, db.ForeignKey("articles.id"))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# 防止重复收藏
__table_args__ = (
db.UniqueConstraint("user_id", "article_id",
name="unique_favorite"),
)
收藏接口
from flask_login import login_required, current_user
@app.route("/article//favorite", methods=["POST"])
@login_required
def toggle_favorite(aid):
article = Article.query.get_or_404(aid)
fav = Favorite.query.filter_by(
user_id=current_user.id,
article_id=aid).first()
if fav:
db.session.delete(fav)
db.session.commit()
return {"favorited": False}
else:
db.session.add(Favorite(
user_id=current_user.id,
article_id=aid))
db.session.commit()
return {"favorited": True}
@app.route("/favorites")
@login_required
def my_favorites():
articles = Article.query.join(Favorite).filter(
Favorite.user_id == current_user.id).all()
return render_template("favorites.html", articles=articles)
后台管理(admin 蓝图)
# views/admin.py
from flask import Blueprint, render_template, request, redirect
from flask_login import login_required, current_user
from functools import wraps
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
def admin_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user.is_admin:
return "无权访问", 403
return f(*args, **kwargs)
return wrapper
@admin_bp.route("/")
@login_required
@admin_required
def dashboard():
stats = {
"articles": Article.query.count(),
"users": User.query.count(),
"favorites": Favorite.query.count(),
}
return render_template("admin/dashboard.html", stats=stats)
@admin_bp.route("/articles")
@login_required
@admin_required
def article_list():
articles = Article.query.order_by(
Article.created_at.desc()).all()
return render_template("admin/articles.html",
articles=articles)
@admin_bp.route("/articles//delete", methods=["POST"])
@login_required
@admin_required
def delete_article(aid):
article = Article.query.get_or_404(aid)
db.session.delete(article)
db.session.commit()
flash("文章已删除", "success")
return redirect(url_for("admin.article_list"))
收藏用唯一约束防重复;后台用 admin_required 装饰器统一鉴权;本站 CodeNest 也有类似的管理后台(admin.html)。
打包上线
掌握 Flask 打包上线:完整部署流程。
上线前检查
- DEBUG=False
- SECRET_KEY 从环境变量读取
- 生产数据库配置
- 依赖锁定(requirements.txt)
- 数据库迁移已应用
准备部署文件
project/
├── app.py / run.py
├── config.py
├── requirements.txt
├── .env # 生产环境变量(不入库)
└── migrations/ # 迁移文件
pip freeze > requirements.txt
Gunicorn 启动
pip install gunicorn
# 启动(4 worker,2 线程)
gunicorn -w 4 --threads 2 -b 127.0.0.1:8000 "app:create_app()"
# 或应用文件方式
gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app
wsgi.py
# wsgi.py(Gunicorn 入口)
from app import create_app
app = create_app()
Nginx 配置
server {
listen 80;
server_name www.laowang.click;
location /static/ {
alias /www/wwwroot/app/static/;
expires 7d;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
宝塔面板部署
- 网站 → Python 项目 → 创建项目
- 选择 Python 版本,上传代码
- 安装依赖:
pip install -r requirements.txt - 执行迁移:
flask db upgrade - 启动命令:
gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app - 打开外网映射(自动 Nginx 反代)
- 申请 SSL 证书
上线验证
curl -I https://www.laowang.click/ # 200
curl http://127.0.0.1:8000/api/health # 健康检查
tail -f /var/log/nginx/error.log # 查错误
部署四步:依赖安装 → 数据库迁移 → Gunicorn 启动 → Nginx 反代。本站 CodeNest(FastAPI)部署方式与此类似。
https://palletsprojects.com/projects/flask/
Flask 官方资源:Flask 官方文档与社区。
官方文档
| 资源 | 地址/说明 |
|---|---|
| Flask 官方文档 | flask.palletsprojects.com(权威) |
| Flask 快速上手 | 文档首篇教程,必读 |
| Flask API 参考 | 全部 API 细节 |
| 扩展索引 | 官方维护的扩展列表 |
项目托管
- GitHub:pallets/flask
- PyPI:pypi.org/project/flask
- 中文社区:flask 相关博客与教程
核心扩展(Pallets 官方)
| 扩展 | 用途 |
|---|---|
| Flask-SQLAlchemy | ORM |
| Flask-Migrate | 迁移 |
| Flask-Login | 认证 |
| Flask-WTF | 表单 |
| Flask-Caching | 缓存 |
学习资源
- Flask Mega-Tutorial(Miguel Grinberg)
- 《Flask Web 开发》(Miguel Grinberg 著)
- 官方文档的 Tutorial 部分
- CodeNest 本站 FastAPI 教程对照学习
遇到问题先查官方文档,其次搜 Stack Overflow;Flask 生态成熟,绝大多数问题都有现成答案。