Flask 工程化:应用工厂与蓝图
上一篇 Flask 快速上手 里的单文件写法,超过三个接口就开始难受了。 这篇讲 Flask 官方推荐的工程化组织方式:应用工厂(Application Factory)+ 蓝图(Blueprint)。
1. 单文件写法会撞上什么
# app.py —— 一开始这样写没问题
from flask import Flask
from models import User # ← 循环导入的开始
app = Flask(__name__)规模一上来,四个问题会同时出现:
根因是同一个:应用实例 app 在模块导入时就被创建了,于是所有人都得 import 它,循环依赖无法避免。
解法是把它推迟到「工厂函数被调用时」再创建。
2. 应用工厂(Application Factory)
把创建 app 的逻辑包进一个函数:
# app/__init__.py
from flask import Flask
def create_app(config_object="app.config.ProductionConfig"):
app = Flask(__name__)
# 1) 加载配置
app.config.from_object(config_object)
# 2) 初始化扩展(此时 app 才存在)
from app.extensions import db, migrate
db.init_app(app)
migrate.init_app(app, db)
# 3) 注册蓝图
from app.views.users import users_bp
from app.views.posts import posts_bp
app.register_blueprint(users_bp)
app.register_blueprint(posts_bp)
# 4) 注册错误处理器 / CLI 命令
from app.errors import register_error_handlers
register_error_handlers(app)
return app为什么这样就解决了循环导入:模块顶部不再有 app = Flask(__name__) 这个全局实例,app.views.users 里只需要 from app.extensions import db,而 app/__init__.py 的 import 又都写在函数体内(延迟到调用时执行)。
一个容易忽略的收益
create_app("app.config.TestingConfig") 可以造出任意配置的实例。测试时每个用例用独立配置建一个 app,互不污染——单文件写法做不到这点。
3. 扩展的延迟初始化
关键点:扩展对象在模块层创建,但绑定 app 的动作放到工厂里。
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy() # 注意:这里不传 app
migrate = Migrate()# 在 create_app 里
db.init_app(app)所有主流 Flask 扩展都遵循这个 init_app(app) 约定。这样 models.py 可以放心写 from app.extensions import db,不会牵出 app。
4. 蓝图(Blueprint)
蓝图是「一组路由 + 模板 + 静态文件的集合」,用来按业务模块拆分代码。
# app/views/users.py
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import User
# 三个参数:蓝图名(用于 url_for 前缀)、所在模块、URL 前缀
users_bp = Blueprint("users", __name__, url_prefix="/api/users")
@users_bp.get("/")
def list_users():
users = db.session.execute(db.select(User)).scalars().all()
return jsonify([u.to_dict() for u in users])
@users_bp.get("/<int:user_id>")
def get_user(user_id: int):
user = db.session.get(User, user_id)
if user is None:
return {"error": "用户不存在"}, 404
return user.to_dict()
@users_bp.post("/")
def create_user():
data = request.get_json(silent=True) or {}
user = User(name=data.get("name", ""))
db.session.add(user)
db.session.commit()
return user.to_dict(), 201注册后,实际路径是 url_prefix + 路由:
| 蓝图内定义 | 注册后实际路径 |
|---|---|
@users_bp.get("/") | /api/users/ |
@users_bp.get("/<int:user_id>") | /api/users/<int:user_id> |
4.1 蓝图带来的两个变化
端点名变了:url_for 要带蓝图名前缀。
url_for("users.get_user", user_id=42) # 不是 "get_user"app 换成了 current_app:蓝图里拿不到 app 实例,用 current_app 代理取配置。
from flask import current_app
max_size = current_app.config["MAX_CONTENT_LENGTH"]4.2 什么时候该拆蓝图
按业务资源拆,不按技术层拆:
- ✅
users/posts/comments/admin - ❌
models/services/utils(这些是包,不是蓝图)
一个蓝图对应一个 URL 前缀,边界清楚,日后要拆成独立服务也容易。
5. 配置分层
# app/config.py
import os
class Config:
"""公共配置"""
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-only-change-me")
SQLALCHEMY_TRACK_MODIFICATIONS = False
JSON_SORT_KEYS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"] # 必须显式提供
config_map = {
"development": DevelopmentConfig,
"testing": TestingConfig,
"production": ProductionConfig,
}再由环境变量决定加载哪一份:
# app/__init__.py
def create_app(config_name: str | None = None):
app = Flask(__name__)
config_name = config_name or os.environ.get("FLASK_CONFIG", "development")
app.config.from_object(config_map[config_name])
...生产配置必须来自环境变量
SECRET_KEY、DATABASE_URL 这类绝不能写死在代码里提交到仓库。用 os.environ[...] 强制要求提供,缺失时直接启动失败比带着默认值上线安全得多。
想让 flask run 自动读 .env:
pip install "Flask[dotenv]"# .env(加入 .gitignore)
FLASK_CONFIG=development
SECRET_KEY=local-dev-secret
DATABASE_URL=postgresql://user:pass@localhost:5432/app6. 完整目录结构
flask-app/
├── app/
│ ├── __init__.py # create_app 工厂
│ ├── config.py # 配置分层
│ ├── extensions.py # db / migrate 等扩展实例
│ ├── errors.py # 统一错误处理器
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py
│ ├── views/ # 蓝图(路由层)
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── posts.py
│ ├── services/ # 业务逻辑(可选,逻辑变复杂时再加)
│ ├── templates/
│ │ ├── base.html
│ │ └── users/
│ │ └── list.html
│ └── static/
│ └── style.css
├── migrations/ # Alembic 迁移(flask db init 生成)
├── tests/
│ ├── conftest.py
│ └── test_users.py
├── .env # 不进仓库
├── .env.example # 进仓库,作为模板
├── requirements.txt
└── wsgi.py # 生产入口wsgi.py 只做一件事:
from app import create_app
app = create_app()7. 启动方式
7.1 开发
# 工厂模式下用 --app 指向工厂函数
flask --app app:create_app run --debug
# 或者指向 wsgi.py 里的 app
flask --app wsgi run --debug7.2 生产
Linux 用 gunicorn,Windows 用 waitress
gunicorn 依赖 fcntl,在 Windows 上装不了也跑不起来。Windows 服务器请用 waitress。
# Linux / macOS
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
# Windows
pip install waitress
waitress-serve --host=0.0.0.0 --port=8000 --call app:create_appworker 数量经验值:2 × CPU 核数 + 1,I/O 密集型可适当多给。
⚠️ 多进程意味着内存不共享。模块级的全局字典做缓存、计数器、在线用户列表,在 4 个 worker 下会变成 4 份互不可见的数据。要么改用 Redis,要么接受它只是「每进程缓存」。
8. 测试
工厂模式让测试变得干净:
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db
@pytest.fixture
def app():
app = create_app("app.config.TestingConfig")
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_users.py
def test_create_user(client):
resp = client.post("/api/users/", json={"name": "Alice"})
assert resp.status_code == 201
assert resp.get_json()["name"] == "Alice"
def test_get_missing_user(client):
assert client.get("/api/users/999").status_code == 404每个测试拿到的是全新的 app 和内存数据库,用例之间不会互相污染。
9. 常见坑
| 现象 | 原因 | 解决 |
|---|---|---|
RuntimeError: Working outside of application context | 在请求/应用上下文之外用了 db 或 current_app | 用 with app.app_context(): 包住 |
url_for 报 BuildError | 蓝图端点漏了蓝图名前缀 | 写成 url_for("users.get_user", ...) |
| 蓝图路由 404 | url_prefix 和路由都带斜杠,或末尾斜杠不匹配 | url_prefix 不写结尾斜杠;注意 /users 与 /users/ 的区别 |
flask db upgrade 说没找到 app | 工厂模式下 Flask CLI 找不到实例 | 用 flask --app app:create_app db upgrade |
| 多个 worker 下缓存/计数不对 | 进程内存不共享 | 改用 Redis 或外部存储 |
Windows 上 pip install gunicorn 失败 | gunicorn 依赖 fcntl | 改用 waitress |
| 改了模板不生效 | 生产模式模板有缓存 | 开发用 --debug;或确认没有误开 TEMPLATES_AUTO_RELOAD=False |
| 循环导入又出现了 | 在模块顶层 import 了 create_app | 工厂内的 import 保持在函数体里 |
SECRET_KEY 用了默认值上线 | 配置里给了兜底默认值 | 生产配置用 os.environ[...] 强制要求 |
10. 小结
| 主题 | 关键点 |
|---|---|
| 应用工厂 | create_app() 把 app 创建推迟到调用时,根治循环导入,并让多环境/测试成为可能 |
| 扩展初始化 | 扩展对象在模块层创建,init_app(app) 在工厂里绑定 |
| 蓝图 | 按业务资源拆分路由,url_prefix 决定前缀;url_for 要带蓝图名前缀 |
| 上下文代理 | 蓝图里用 current_app 取配置,用 g 存请求级临时数据 |
| 配置分层 | Config 基类 + 环境子类,由 FLASK_CONFIG 选择;敏感项强制走环境变量 |
| 生产启动 | Linux 用 gunicorn,Windows 用 waitress;多 worker 下不要依赖进程内状态 |
| 测试 | 每个用例 create_app(TestingConfig) + 内存库,天然隔离 |
📎 相关阅读:
- FastAPI 与 Flask 深度对比 —— 想清楚该不该继续用 Flask
- Flask 快速上手 —— 单文件用法速查