使用蓝图模块化应用

Changelog

0.7 新版功能.

为了在一个或多个应用中支持模式复用,Flask 引入了蓝图的概念。蓝图可以极大地简化大型应用的运作方式并且为 Flask 的扩展提供了一个集中注册应用修改操作的入口。Blueprint 对象与 Flask 应用对象的工作方式类似,但它不是一个真正的应用,而是一个记录了如何构造或扩展应用的 蓝图

为何要使用蓝图?

Flask 中的蓝图为以下场景设计:

  • 把一个应用分解为一组蓝图。这是针对大型应用的理想方案:一个项目可以创建一个应用实例,初始化多个扩展,注册一系列蓝图。

  • 在应用的某个 URL 前缀和(或)子域上注册一个蓝图。在此URL 前缀和(或)子域上的参数将成为蓝图中所有视图的共用参数。

  • 在一个应用上使用不同 URL 规则多次注册同一个蓝图。

  • 通过蓝图提供模板过滤器、静态文件、模板及其他通用工具。蓝图并非必须实现应用或视图函数。

  • 初始化 Flask 扩展时,为以上任意一种用途注册一个蓝图。

Flask 中的蓝图并非一个可插拔的应用,因为它实际上不是一个应用——它是一组可注册在应用对象上的操作的集合,甚至可以注册多次。那么为什么不用多个应用对象呢?你可以这么做(参见 Application Dispatching),但这样会导致你的应用各自拥有独立的配置,且只能在 WSGI 层管理。

而蓝图能在 Flask 应用层面隔离,共享应用配置,并在注册时按需修改应用对象。它的不足之处是一旦应用对象被创建,蓝图就不能注销,除非销毁整个应用对象。

蓝图的理念

蓝图的基本理念是它记录了注册到应用之后将要执行的操作。在分配请求及生成两个端点间的URL时,Flask 会把蓝图和视图函数关联起来。

第一个蓝图

以下是一个最基础的蓝图示例。在此例中,我们使用了蓝图来简单地渲染静态模板:

from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound

simple_page = Blueprint('simple_page', __name__,
                        template_folder='templates')

@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)

当你使用 @simple_page.route 装饰器绑定一个函数时,蓝图会记录下当它被注册到应用上时即将进行的操作,即把 show 函数注册到应用上。此外它还会在函数的端点中加上一个前缀,也就是构造 Blueprint 时所使用的名称(本例中即 simple_page)。蓝图的名称不会改变 URL,只改变端点。

注册蓝图

如何注册此蓝图?可以这样:

from flask import Flask
from yourapplication.simple_page import simple_page

app = Flask(__name__)
app.register_blueprint(simple_page)

如果你查看应用上注册的 URL 规则,你可以看到:

>>> app.url_map
Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
 <Rule '/<page>' (HEAD, OPTIONS, GET) -> simple_page.show>,
 <Rule '/' (HEAD, OPTIONS, GET) -> simple_page.show>])

第一条很明显来自应用本身的静态文件服务,另外两条是用于 simple_page 蓝图的 show 函数的。如你所见,它们的前缀都是蓝图的名称,用一个点(.)分隔

蓝图还可以挂载到不同的位置:

app.register_blueprint(simple_page, url_prefix='/pages')

那么它生成的规则就自然是:

>>> app.url_map
Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
 <Rule '/pages/<page>' (HEAD, OPTIONS, GET) -> simple_page.show>,
 <Rule '/pages/' (HEAD, OPTIONS, GET) -> simple_page.show>])

在此基础上,你还可以多次注册蓝图,尽管不是每个蓝图都能正确响应。蓝图是否能多次注册实际上取决于蓝图的实现方式。

嵌套的蓝图

你可以把蓝图注册到另一个蓝图上。

parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
parent.register_blueprint(child)
app.register_blueprint(parent)

子蓝图的名称会以父蓝图的名称作为前缀,子蓝图的 URL 也会以父蓝图的 URL 前缀作为前缀。

url_for('parent.child.create')
/parent/child/create

注册在父蓝图上的请求前钩子及其他钩子也会在子蓝图上触发。如果子蓝图未给某错误指定处理函数,会去寻找父蓝图上的错误处理函数。

蓝图资源

蓝图也能够提供资源,有时可能仅仅是为了提供资源而引入蓝图。

蓝图资源文件夹

与普通应用类似,蓝图一般都放在一个文件夹中。虽然多个蓝图可以来源于同个文件夹,但你不必这么做,这也不是通常推荐的做法。

文件夹是从 Blueprint 接受的第二个参数推断而来,通常为 __name__。此参数指定了蓝图所对应的 Python 模块或者包。如果它指向了一个实际存在的 Python 包(也就是文件系统中的一个文件夹),那么这个包即为资源文件夹;如果它是一个模块,包含该模块的包即为资源文件夹。你可以访问 Blueprint.root_path 属性来查看资源文件夹:

>>> simple_page.root_path
'/Users/username/TestProject/yourapplication'

使用 open_resource() 方法来快速打开文件夹中的资源:

with simple_page.open_resource('static/style.css') as f:
    code = f.read()

静态文件

你可以把文件夹的路径传给蓝图的 static_folder 参数来让蓝图提供静态文件,它既可以是绝对路径,也可以是相对于蓝图路径的相对路径:

admin = Blueprint('admin', __name__, static_folder='static')

默认情况下,路径最右端的部分将作为静态文件的 URL,可以通过指定 static_url_path 来改变。因为上例中文件夹名称是 static,所以静态文件可以通过蓝图的 url_prefix 加上 /static 访问。比如蓝图的 URL 前缀是 /admin,则静态文件的 URL 为 /admin/static

端点的名称是 blueprint_name.static。你可以使用 url_for() 来生成 URL,和应用中的静态文件夹一样:

url_for('admin.static', filename='style.css')

然而,如果蓝图没有 url_prefix 属性,你将不能访问蓝图中的静态文件。这是因为这个情况下 URL 会是 /static,而应用级的 /static 路由会优先匹配。和模板文件夹不同,当 Flask 在应用的静态文件夹中找不到文件时,不会去搜索蓝图的静态文件夹。

模板

如果你想在蓝图中暴露模板文件,你可以给 Blueprint 指定 template_folder 参数:

admin = Blueprint('admin', __name__, template_folder='templates')

对静态文件来说,路径可以是绝对路径,也可以相对于蓝图的资源文件夹。

模板文件夹会被添加到模板的搜索路径中,但比应用的模板文件夹优先级更低。这样你可以很容易地在应用中覆写蓝图提供的模板。这也意味着如果你不希望蓝图模板被意外覆盖,你需要保证模板的相对路径与其他蓝图或应用的模板都不相同。如果有多个模板有相同的模板相对路径,第一个被注册的蓝图中的模板将被选中。

因此,如果你的蓝图位于 yourapplication/admin 中,你想渲染模板 'admin/index.html' 并且你指定了 template_foldertemplates,那么你必须将模板创建为 yourapplication/admin/templates/admin/index.html。 其中包含一个额外的 admin 是为了防止模板被应用模板文件夹中一个名叫 index.html 的模板文件所覆盖。

进一步阐明:如果你有一个名为 admin 的蓝图,你希望渲染蓝图的模板 index.html,最好按照如下方式存放模板文件:

yourpackage/
    blueprints/
        admin/
            templates/
                admin/
                    index.html
            __init__.py

当你需要使用此模板时,使用 admin/index.html 作为查找模板的名称。如果在加载模板时遇到任何问题,启用 EXPLAIN_TEMPLATE_LOADING 配置变量,它可以在每次 reder_template 调用时让 Flask 打印查找模板的步骤。

构造 URL

要从一个页面链接到其他页面,你可以像之前一样使用 url_for() 函数,只不过你需要在 URL 端点前面加上蓝图的名称以及一个点(.):

url_for('admin.index')

另外,如果在一个蓝图的视图函数或者渲染的模板中需要链接到相同蓝图的其他端点,你可以使用相对重定向,只要在端点前面加上一个点就可以了:

url_for('.index')

只要当前请求被分发到任一 admin 蓝图的端点时,上例的 URL 会链接到 admin.index

蓝图的错误处理器

Flask 应用对象一样,蓝图也提供了 errorhandler 装饰器,所以创建蓝图特定的自定义错误页面非常容易。

下面是一个 “404 Page Not Found” 异常的例子:

@simple_page.errorhandler(404)
def page_not_found(e):
    return render_template('pages/404.html')

大多数错误处理器会按预期工作,然而,关于 404 和 405 错误处理有件事需要注意:它们只能由在此蓝图的其他视图函数中的 raise 语句或 abort 触发,而不能由类似于无效的 URL 访问这种错误触发。这是因为蓝图并不“拥有”某个 URL 空间,所以当无效的 URL 访问发生时,应用实例无从得知应该运行哪个蓝图的错误处理器。如果你想基于 URL 前缀分别执行不同的错误处理策略,那么可以在应用层面使用 request 代理对象来区分定义:

@app.errorhandler(404)
@app.errorhandler(405)
def _handle_api_error(ex):
    if request.path.startswith('/api/'):
        return jsonify(error=str(ex)), ex.code
    else:
        return ex

参见 Handling Application Errors