带有蓝图的模块化应用程序

变更日志

0.7 版新增。

Flask 使用蓝图概念来制作应用程序组件,并在应用程序内或跨应用程序支持常见模式。蓝图可以极大地简化大型应用程序的工作方式,并为 Flask 扩展提供注册应用程序操作的中心化方式。一个 Blueprint 对象类似于 Flask 应用程序对象,但它实际上并不是一个应用程序。相反,它是如何构建或扩展应用程序的蓝图

为什么使用蓝图?

Flask 中的蓝图适用于以下情况

  • 将应用程序分解为一组蓝图。这非常适合大型应用程序;项目可以实例化一个应用程序对象,初始化多个扩展,并注册一组蓝图。

  • 在 URL 前缀和/或子域中向应用程序注册蓝图。URL 前缀/子域中的参数成为蓝图中所有视图函数的通用视图参数(带默认值)。

  • 使用不同的 URL 规则在应用程序上多次注册蓝图。

  • 通过蓝图提供模板过滤器、静态文件、模板和其他实用程序。蓝图不必实现应用程序或视图函数。

  • 在初始化 Flask 扩展时,出于以下任何原因在应用程序上注册蓝图。

Flask 中的蓝图不是可插入的应用程序,因为它实际上不是一个应用程序,而是一组可以在应用程序上注册的操作,甚至可以多次注册。为什么不使用多个应用程序对象?你可以这样做(请参阅 应用程序分发),但你的应用程序将具有单独的配置,并且将在 WSGI 层进行管理。

相反,蓝图在 Flask 层面提供分离,共享应用程序配置,并且可以在注册时根据需要更改应用程序对象。缺点是,一旦创建应用程序,你就无法取消注册蓝图,而无需销毁整个应用程序对象。

蓝图的概念

蓝图的基本概念是,它们记录在应用程序上注册时要执行的操作。Flask 在分发请求和从一个端点生成到另一个端点的 URL 时将视图函数与蓝图关联起来。

我的第一个蓝图

这是一个非常基本的蓝图的外观。在这种情况下,我们希望实现一个蓝图,它可以简单地呈现静态模板

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)

如果您检查在应用程序上注册的规则,您会发现这些

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

此外,子蓝图将获得其父蓝图的子域,如果存在,则以其子域作为前缀,即

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

url_for('parent.child.create', _external=True)
"child.parent.domain.tld"

与父蓝图一起注册的蓝图特定的请求前函数等将触发子蓝图。如果子蓝图没有可以处理给定异常的错误处理程序,则将尝试父蓝图的错误处理程序。

蓝图资源

蓝图也可以提供资源。有时您可能只想引入一个蓝图,仅用于它提供的资源。

蓝图资源文件夹

与常规应用程序一样,蓝图被认为包含在一个文件夹中。虽然多个蓝图可以源自同一个文件夹,但不必如此,而且通常不建议这样做。

文件夹从 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')

默认情况下,路径的最右侧部分是它在 Web 上公开的位置。可以使用 static_url_path 参数更改此设置。由于此处文件夹称为 static,因此它将在蓝图的 url_prefix + /static 中可用。如果蓝图的前缀为 /admin,则静态 URL 将为 /admin/static

端点被命名为 blueprint_name.static。您可以使用 url_for() 为其生成 URL,就像使用应用程序的静态文件夹一样

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

但是,如果蓝图没有 url_prefix,则无法访问蓝图的静态文件夹。这是因为在这种情况下,URL 将为 /static,并且应用程序的 /static 路由优先。与模板文件夹不同,如果文件不存在于应用程序静态文件夹中,则不会搜索蓝图静态文件夹。

模板

如果您希望蓝图公开模板,可以通过向 Blueprint 构造函数提供 template_folder 参数来实现

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

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

模板文件夹被添加到模板的搜索路径中,但优先级低于实际应用程序的模板文件夹。这样,您可以轻松覆盖蓝图在实际应用程序中提供的模板。这也意味着,如果您不希望蓝图模板被意外覆盖,请确保没有其他蓝图或实际应用程序模板具有相同的相对路径。当多个蓝图提供相同的相对模板路径时,第一个注册的蓝图优先于其他蓝图。

因此,如果您在文件夹 yourapplication/admin 中有一个蓝图,并且您想要呈现模板 'admin/index.html',并且您已提供 templates 作为 template_folder,您将不得不创建一个这样的文件: 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 配置变量,这将指示 Flask 在每次 render_template 调用时打印出它用来查找模板的步骤。

构建 URL

如果您想从一个页面链接到另一个页面,您可以使用 url_for() 函数,就像您通常会做的那样,只是您将 URL 端点加上蓝图的名称和一个点 (.)

url_for('admin.index')

此外,如果您处于蓝图或呈现的模板的视图函数中,并且您想链接到同一蓝图的另一个端点,您可以仅通过一个点来给端点加上前缀,从而使用相对重定向

url_for('.index')

例如,这将链接到 admin.index,以防当前请求被分派到任何其他 admin 蓝图端点。

蓝图错误处理程序

蓝图支持 errorhandler 装饰器,就像 Flask 应用程序对象一样,因此很容易制作蓝图特定的自定义错误页面。

以下是“404 页面未找到”异常的示例

@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

参见 处理应用程序错误