跳到主要内容

Blueprints

当你的 Flask 应用逐渐变大时,把所有代码放在一个文件里会变得难以维护。Blueprints(蓝图)是 Flask 提供的一种组织应用结构的方式,它让你能够将应用拆分为多个逻辑组件,每个组件可以有自己的路由、模板、静态文件和错误处理器。

理解 Blueprint 的本质

Blueprint 不是一个独立的应用,而是一组"待注册"的操作记录。当你在 Blueprint 上定义路由、错误处理器或模板过滤器时,Flask 只是记录下这些操作,直到你将 Blueprint 注册到应用上,这些操作才会真正生效。

这种设计带来了几个重要的特点:

  1. 延迟绑定:Blueprint 可以独立定义,然后在需要时注册到应用
  2. 可复用:同一个 Blueprint 可以注册多次,每次使用不同的 URL 前缀
  3. 共享配置:与创建多个 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 有以下特性:

  1. 名称继承:子 Blueprint 的端点会加上父 Blueprint 的名称前缀
  2. URL 前缀合并:子 Blueprint 的 URL 前缀会与父 Blueprint 的合并
  3. 请求钩子传递:父 Blueprint 的 before_request 等钩子对子 Blueprint 生效
  4. 错误处理器传递:如果子 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'
)

模板查找规则:

  1. 优先查找应用的模板目录
  2. 然后查找各 Blueprint 的模板目录
  3. 多个 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 的各个方面:

  1. Blueprint 本质:理解 Blueprint 是操作记录,不是独立应用
  2. 注册方式:URL 前缀、子域名、多次注册
  3. 嵌套 Blueprint:父子关系和继承行为
  4. 资源管理:静态文件、模板的正确组织方式
  5. 请求钩子:Blueprint 级别的请求处理
  6. 错误处理:404/405 的特殊处理方式
  7. 完整示例:模块化博客应用的实现

使用 Blueprint 可以让你的应用结构更清晰、代码更易维护,是构建大型 Flask 应用的必备技能。

参考资料