Skip to content

Flask 工程化:应用工厂与蓝图 ​

上一篇 Flask 快速上手 里的单文件写法,超过三个接口就开始难受了。 这篇讲 Flask 官方推荐的工程化组织方式:应用工厂(Application Factory)+ 蓝图(Blueprint)。


1. 单文件写法会撞上什么 ​

python
# app.py —— 一开始这样写没问题
from flask import Flask
from models import User          # ← 循环导入的开始

app = Flask(__name__)

规模一上来,四个问题会同时出现:

根因是同一个:应用实例 app 在模块导入时就被创建了,于是所有人都得 import 它,循环依赖无法避免。

解法是把它推迟到「工厂函数被调用时」再创建。


2. 应用工厂(Application Factory) ​

把创建 app 的逻辑包进一个函数:

python
# 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 的动作放到工厂里。

python
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()          # 注意:这里不传 app
migrate = Migrate()
python
# 在 create_app 里
db.init_app(app)

所有主流 Flask 扩展都遵循这个 init_app(app) 约定。这样 models.py 可以放心写 from app.extensions import db,不会牵出 app。


4. 蓝图(Blueprint) ​

蓝图是「一组路由 + 模板 + 静态文件的集合」,用来按业务模块拆分代码。

python
# 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 要带蓝图名前缀。

python
url_for("users.get_user", user_id=42)   # 不是 "get_user"

app 换成了 current_app:蓝图里拿不到 app 实例,用 current_app 代理取配置。

python
from flask import current_app

max_size = current_app.config["MAX_CONTENT_LENGTH"]

4.2 什么时候该拆蓝图 ​

按业务资源拆,不按技术层拆:

  • ✅ users / posts / comments / admin
  • ❌ models / services / utils(这些是包,不是蓝图)

一个蓝图对应一个 URL 前缀,边界清楚,日后要拆成独立服务也容易。


5. 配置分层 ​

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

再由环境变量决定加载哪一份:

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

bash
pip install "Flask[dotenv]"
ini
# .env(加入 .gitignore)
FLASK_CONFIG=development
SECRET_KEY=local-dev-secret
DATABASE_URL=postgresql://user:pass@localhost:5432/app

6. 完整目录结构 ​

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 只做一件事:

python
from app import create_app

app = create_app()

7. 启动方式 ​

7.1 开发 ​

bash
# 工厂模式下用 --app 指向工厂函数
flask --app app:create_app run --debug

# 或者指向 wsgi.py 里的 app
flask --app wsgi run --debug

7.2 生产 ​

Linux 用 gunicorn,Windows 用 waitress

gunicorn 依赖 fcntl,在 Windows 上装不了也跑不起来。Windows 服务器请用 waitress。

bash
# 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_app

worker 数量经验值:2 × CPU 核数 + 1,I/O 密集型可适当多给。

⚠️ 多进程意味着内存不共享。模块级的全局字典做缓存、计数器、在线用户列表,在 4 个 worker 下会变成 4 份互不可见的数据。要么改用 Redis,要么接受它只是「每进程缓存」。


8. 测试 ​

工厂模式让测试变得干净:

python
# 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()
python
# 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", ...)
蓝图路由 404url_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) + 内存库,天然隔离

📎 相关阅读:

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