Blueprints
当你的 Flask 应用逐渐变大时,把所有代码放在一个文件里会变得难以维护。Blueprints(蓝图)是 Flask 提供的一种组织应用结构的方式,它让你能够将应用拆分为多个逻辑组件,每个组件可以有自己的路由、模板、静态文件和错误处理器。
理解 Blueprint 的本质
Blueprint 不是一个独立的应用,而是一组"待注册"的操作记录。当你在 Blueprint 上定义路由、错误处理器或模板过滤器时,Flask 只是记录下这些操作,直到你将 Blueprint 注册到应用上,这些操作才会真正生效。
这种设计带来了几个重要的特点:
- 延迟绑定:Blueprint 可以独立定义,然后在需要时注册到应用
- 可复用:同一个 Blueprint 可以注册多次,每次使用不同的 URL 前缀
- 共享配置:与创建多个 Flask 应用不同,Blueprint 共享同一个应用配置
Blueprint vs 多应用
你可能会问:为什么不直接创建多个 Flask 应用?
虽然可以创建多个应用实例,但它们会有各自独立的配置,并且需要在 WSGI 层面进行分发管理。Blueprint 在 Flask 层面提供了更好的组织方式,所有 Blueprint 共享同一个应用配置和扩展。
Blueprint 的一个限制是:一旦注册就无法取消(除非销毁整个应用对象)。
第一个 Blueprint
让我们从一个简单的例子开始,创建一个负责渲染静态页面的 Blueprint:
# simple_page.py
from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound
# 创建 Blueprint
# 第一个参数是 Blueprint 的名称
# 第二个参数通常是 __name__,用于确定资源路径
simple_page = Blueprint(
'simple_page',
__name__,
template_folder='templates'
)
# 定义路由(注意使用 Blueprint 对象而不是 app)
@simple_page.route('/', defaults={'page': 'index'})
@simple_page.route('/<page>')
def show(page):
"""渲染指定页面"""
try:
return render_template(f'pages/{page}.html')
except TemplateNotFound:
abort(404)
然后将其注册到应用:
# app.py
from flask import Flask
from simple_page import simple_page
app = Flask(__name__)
# 注册 Blueprint
app.register_blueprint(simple_page)
注册后,你可以检查应用的 URL 映射:
# 查看所有注册的路由
for rule in app.url_map.iter_rules():
print(f'{rule.endpoint}: {rule.rule}')
# 输出:
# static: /static/<filename>
# simple_page.show: /<page>
# simple_page.show: /
注意路由的端点名称自动加上了 Blueprint 名称作为前缀(simple_page.show)。
注册 Blueprint
基本 URL 前缀
Blueprint 可以挂载到特定的 URL 前缀下:
# 注册时添加 URL 前缀
app.register_blueprint(simple_page, url_prefix='/pages')
# 现在路由变为:
# /pages/ -> simple_page.show
# /pages/<page> -> simple_page.show
URL 前缀中的变量会成为所有视图函数的默认参数:
# 带变量的 URL 前缀
app.register_blueprint(user_bp, url_prefix='/user/<int:user_id>')
# 在视图中自动获得 user_id 参数
@user_bp.route('/profile')
def profile(user_id): # user_id 自动传入
return f'User {user_id} profile'
子域名支持
Blueprint 可以挂载到子域名:
api_bp = Blueprint('api', __name__, subdomain='api')
# 配置服务器名(必须)
app.config['SERVER_NAME'] = 'example.com:5000'
# 访问 api.example.com:5000/users
@api_bp.route('/users')
def list_users():
return {'users': []}
app.register_blueprint(api_bp)
多次注册
同一个 Blueprint 可以注册多次,每次使用不同的配置:
# 创建一个通用的 API Blueprint
api_v1 = Blueprint('api', __name__)
@api_v1.route('/users')
def list_users():
return {'users': []}
# 注册到不同的 URL 前缀
app.register_blueprint(api_v1, url_prefix='/api/v1', name='api_v1')
app.register_blueprint(api_v1, url_prefix='/api/v2', name='api_v2')
# 现在有两个端点:
# /api/v1/users -> api_v1.list_users
# /api/v2/users -> api_v2.list_users
注意:不是所有 Blueprint 都能正确处理多次注册,这取决于 Blueprint 的具体实现。
嵌套 Blueprint
Flask 支持将 Blueprint 注册到另一个 Blueprint 上,形成嵌套结构:
# 创建父子 Blueprint
parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
@child.route('/create')
def create():
return 'Child create'
# 将子 Blueprint 注册到父 Blueprint
parent.register_blueprint(child)
# 将父 Blueprint 注册到应用
app.register_blueprint(parent)
嵌套后的效果:
# 子 Blueprint 获得:
# 1. 名称前缀:parent.child
# 2. URL 前缀:/parent/child
# URL 生成
url_for('parent.child.create') # 返回 /parent/child/create
嵌套 Blueprint 的行为
嵌套 Blueprint 有以下特性:
- 名称继承:子 Blueprint 的端点会加上父 Blueprint 的名称前缀
- URL 前缀合并:子 Blueprint 的 URL 前缀会与父 Blueprint 的合并
- 请求钩子传递:父 Blueprint 的
before_request等钩子对子 Blueprint 生效 - 错误处理器传递:如果子 Blueprint 没有处理某个异常,会尝试父 Blueprint 的处理器
@parent.before_request
def parent_before():
print('Parent before_request')
@child.before_request
def child_before():
print('Child before_request')
# 访问 /parent/child/create 时
# 输出顺序:Parent before_request -> Child before_request
Blueprint 资源
资源文件夹
每个 Blueprint 都有一个资源文件夹概念,默认是 Blueprint 所在的 Python 模块路径:
# 查看 Blueprint 的资源路径
print(simple_page.root_path)
# 输出类似:/Users/username/project/myapp/simple_page
可以使用 open_resource() 方法访问资源文件:
@simple_page.route('/style')
def get_style():
with simple_page.open_resource('static/style.css') as f:
return f.read()
静态文件
Blueprint 可以有自己的静态文件目录:
admin = Blueprint(
'admin',
__name__,
static_folder='static', # 静态文件目录
static_url_path='/static' # URL 路径(相对于 Blueprint 前缀)
)
# 如果 Blueprint 的 url_prefix 是 /admin
# 静态文件的 URL 是 /admin/static/<filename>
在模板中生成静态文件 URL:
<link rel="stylesheet" href="{{ url_for('admin.static', filename='style.css') }}">
重要限制:如果 Blueprint 没有设置 url_prefix,它的静态文件目录将无法访问。因为 URL /static 会被应用的静态文件路由优先匹配。
模板
Blueprint 可以提供自己的模板目录:
admin = Blueprint(
'admin',
__name__,
template_folder='templates'
)
模板查找规则:
- 优先查找应用的模板目录
- 然后查找各 Blueprint 的模板目录
- 多个 Blueprint 有同名模板时,先注册的优先
推荐的模板组织方式:
myapp/
├── templates/
│ └── base.html # 应用基础模板
├── admin/
│ ├── __init__.py # 定义 admin Blueprint
│ └── templates/
│ └── admin/ # 避免与应用模板冲突
│ ├── base.html # 继承应用的 base.html
│ └── index.html
└── auth/
├── __init__.py
└── templates/
└── auth/ # 同样使用子目录
├── login.html
└── register.html
这样在渲染时使用 admin/index.html 作为模板名,避免被其他模板覆盖。
调试模板加载:
# 在配置中启用模板加载调试
app.config['EXPLAIN_TEMPLATE_LOADING'] = True
# 每次渲染模板时,Flask 会打印详细的查找过程
URL 构建
在 Blueprint 内部生成 URL 时,需要注意端点名称的格式:
# 假设有 admin Blueprint
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@admin_bp.route('/users')
def list_users():
pass
# 在任何地方都可以这样生成 URL
url_for('admin.list_users') # 返回 /admin/users
# 在 admin Blueprint 内部,可以使用相对端点
@admin_bp.route('/dashboard')
def dashboard():
# 使用 . 前缀表示当前 Blueprint
return redirect(url_for('.list_users')) # 等同于 admin.list_users
相对端点的好处是:即使更改了 Blueprint 名称,也不需要修改代码。
# 在模板中
<a href="{{ url_for('admin.list_users') }}">用户列表</a>
# 跨 Blueprint 链接
<a href="{{ url_for('auth.login') }}">登录</a>
请求钩子
Blueprint 可以定义自己的请求钩子,这些钩子只对 Blueprint 内的路由生效:
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
@auth_bp.before_request
def check_session():
"""每次请求前检查会话"""
if 'user_id' not in session and request.endpoint != 'auth.login':
return redirect(url_for('auth.login'))
@auth_bp.after_request
def add_header(response):
"""添加安全响应头"""
response.headers['X-Auth-Module'] = 'active'
return response
@auth_bp.teardown_request
def cleanup(exception=None):
"""清理资源"""
if exception:
app.logger.error(f'Auth request failed: {exception}')
钩子执行顺序
当应用级钩子和 Blueprint 钩子同时存在时,执行顺序如下:
请求到达
↓
before_first_request(仅第一次)
↓
应用级 before_request
↓
Blueprint 级 before_request
↓
视图函数
↓
Blueprint 级 after_request
↓
应用级 after_request
↓
Blueprint 级 teardown_request
↓
应用级 teardown_request
错误处理
Blueprint 支持定义自己的错误处理器:
@auth_bp.errorhandler(404)
def not_found(error):
return render_template('auth/404.html'), 404
@auth_bp.errorhandler(403)
def forbidden(error):
return render_template('auth/403.html'), 403
404 和 405 的特殊处理
Blueprint 的 404 和 405 错误处理器有特殊限制:它们只能捕获 Blueprint 内部视图函数抛出的异常,无法捕获无效 URL 访问产生的 404。
这是因为 Blueprint 不"拥有"某个 URL 空间,当访问一个不存在的 URL 时,Flask 无法确定应该使用哪个 Blueprint 的错误处理器。
解决方案是在应用级别根据 URL 前缀选择处理方式:
@app.errorhandler(404)
@app.errorhandler(405)
def handle_not_found(error):
# 根据 URL 前缀返回不同的错误响应
if request.path.startswith('/api/'):
return jsonify({'error': 'Not found', 'code': 404}), 404
elif request.path.startswith('/admin/'):
return render_template('admin/404.html'), 404
else:
return render_template('404.html'), 404
完整示例:模块化博客应用
下面是一个完整的模块化博客应用示例:
项目结构
blog/
├── app/
│ ├── __init__.py # 应用工厂
│ ├── config.py # 配置
│ ├── extensions.py # 扩展
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── post.py
│ ├── auth/
│ │ ├── __init__.py # auth Blueprint
│ │ ├── routes.py
│ │ ├── forms.py
│ │ └── templates/
│ │ └── auth/
│ │ ├── login.html
│ │ └── register.html
│ ├── blog/
│ │ ├── __init__.py # blog Blueprint
│ │ ├── routes.py
│ │ ├── forms.py
│ │ └── templates/
│ │ └── blog/
│ │ ├── list.html
│ │ └── detail.html
│ ├── admin/
│ │ ├── __init__.py # admin Blueprint
│ │ ├── routes.py
│ │ └── templates/
│ │ └── admin/
│ │ └── dashboard.html
│ └── templates/
│ └── base.html
├── migrations/
├── tests/
├── requirements.txt
└── run.py
应用工厂
# app/__init__.py
from flask import Flask
from .extensions import db, login_manager, migrate
from .config import Config
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# 初始化扩展
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
# 注册 Blueprint
from .auth import auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from .blog import blog_bp
app.register_blueprint(blog_bp)
from .admin import admin_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
# 注册错误处理器
register_error_handlers(app)
return app
def register_error_handlers(app):
"""注册应用级错误处理器"""
from flask import render_template, request, jsonify
@app.errorhandler(404)
def not_found(error):
if request.path.startswith('/api/'):
return jsonify({'error': 'Not found'}), 404
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
扩展初始化
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
# 配置登录管理器
login_manager.login_view = 'auth.login'
login_manager.login_message = '请先登录后访问此页面'
login_manager.session_protection = 'strong'
Auth Blueprint
# app/auth/__init__.py
from flask import Blueprint
auth_bp = Blueprint(
'auth',
__name__,
url_prefix='/auth',
template_folder='templates'
)
from . import routes
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.urls import url_parse
from . import auth_bp
from .forms import LoginForm, RegistrationForm
from ..extensions import db
from ..models.user import User
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""用户登录"""
if current_user.is_authenticated:
return redirect(url_for('blog.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('用户名或密码错误', 'danger')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
# 安全重定向
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('blog.index')
flash('登录成功!', 'success')
return redirect(next_page)
return render_template('auth/login.html', form=form)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
"""用户注册"""
if current_user.is_authenticated:
return redirect(url_for('blog.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('注册成功!请登录', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
"""用户登出"""
logout_user()
flash('您已退出登录', 'info')
return redirect(url_for('blog.index'))
Blog Blueprint
# app/blog/__init__.py
from flask import Blueprint
blog_bp = Blueprint(
'blog',
__name__,
template_folder='templates'
)
from . import routes
# app/blog/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from . import blog_bp
from .forms import PostForm
from ..extensions import db
from ..models.post import Post
@blog_bp.route('/')
def index():
"""博客首页"""
page = request.args.get('page', 1, type=int)
pagination = Post.query.order_by(Post.created_at.desc()).paginate(
page=page, per_page=10, error_out=False
)
return render_template('blog/list.html', pagination=pagination)
@blog_bp.route('/post/<int:id>')
def detail(id):
"""文章详情"""
post = Post.query.get_or_404(id)
return render_template('blog/detail.html', post=post)
@blog_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create():
"""创建文章"""
form = PostForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
content=form.content.data,
author=current_user
)
db.session.add(post)
db.session.commit()
flash('文章发布成功!', 'success')
return redirect(url_for('blog.detail', id=post.id))
return render_template('blog/create.html', form=form)
Admin Blueprint
# app/admin/__init__.py
from flask import Blueprint
admin_bp = Blueprint(
'admin',
__name__,
url_prefix='/admin',
template_folder='templates'
)
from . import routes
# app/admin/routes.py
from flask import render_template, abort
from flask_login import login_required, current_user
from functools import wraps
from . import admin_bp
from ..extensions import db
from ..models.user import User
from ..models.post import Post
def admin_required(f):
"""管理员权限装饰器"""
@wraps(f)
@login_required
def decorated(*args, **kwargs):
if not current_user.is_admin:
abort(403)
return f(*args, **kwargs)
return decorated
@admin_bp.route('/')
@admin_required
def dashboard():
"""管理面板"""
stats = {
'users': User.query.count(),
'posts': Post.query.count(),
}
return render_template('admin/dashboard.html', stats=stats)
@admin_bp.route('/users')
@admin_required
def list_users():
"""用户管理"""
users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin/users.html', users=users)
@admin_bp.errorhandler(403)
def forbidden(error):
"""权限错误处理"""
return render_template('admin/403.html'), 403
Blueprint 最佳实践
1. 使用应用工厂模式
应用工厂模式让测试和配置更加灵活:
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
# 初始化扩展
db.init_app(app)
# 注册 Blueprint
from .auth import auth_bp
app.register_blueprint(auth_bp)
return app
2. 避免循环导入
使用延迟导入解决循环导入问题:
# 在 __init__.py 中导入路由
from . import routes
# 而不是在 routes.py 中导入 Blueprint
3. 模板命名空间
给 Blueprint 的模板添加命名空间,避免冲突:
templates/
├── base.html
└── auth/
└── login.html # 而不是 templates/login.html
4. 共享功能
对于多个 Blueprint 共享的功能,可以考虑:
# 创建工具 Blueprint(不注册路由)
utils_bp = Blueprint('utils', __name__)
@utils_bp.app_template_filter('datetime')
def datetime_filter(value, format='%Y-%m-%d'):
return value.strftime(format)
# 注册到应用
app.register_blueprint(utils_bp)
5. 配置驱动的注册
根据配置决定是否注册某些 Blueprint:
def create_app():
app = Flask(__name__)
# 始终注册核心 Blueprint
app.register_blueprint(core_bp)
# 根据配置注册可选 Blueprint
if app.config['ENABLE_API']:
app.register_blueprint(api_bp, url_prefix='/api')
if app.config['ENABLE_ADMIN']:
app.register_blueprint(admin_bp, url_prefix='/admin')
return app
小结
本章详细介绍了 Flask Blueprints 的各个方面:
- Blueprint 本质:理解 Blueprint 是操作记录,不是独立应用
- 注册方式:URL 前缀、子域名、多次注册
- 嵌套 Blueprint:父子关系和继承行为
- 资源管理:静态文件、模板的正确组织方式
- 请求钩子:Blueprint 级别的请求处理
- 错误处理:404/405 的特殊处理方式
- 完整示例:模块化博客应用的实现
使用 Blueprint 可以让你的应用结构更清晰、代码更易维护,是构建大型 Flask 应用的必备技能。