在小型项目中,我们可以将代码写在一个文件中,但是到了中大型项目中,这么做就不合适了。所以,我们需要将代码分成多个模块或者包,然后导入使用。但这么做很容易导致循环导入问题,所以,Flask 引入了**蓝图(Blueprint)**的概念。
蓝图是一种组织项目中文件或代码的方式。与把视图和其他代码直接注册到app 的方式不同,蓝图方式是把先它们注册到蓝图,然后在工厂函数中把蓝图注册到app。进而实现了路由分发、静态文件的管理等功能。减少了代码之间的耦合。
蓝图记录在 app 上注册自身时要执行的操作。当收到请求并生成 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('/' )
def show(page):
try:
return render_template(f'pages/{page}.html')
except TemplateNotFound:
abort(404)
当我们使用 @simple_page.route
装饰器绑定一个函数时,蓝图会记录下所装饰的函数(本例中为show()
)。当以后在app中注册蓝图时,这个函数会被注册到app中。另外,它会把 Blueprint
实例对象的名称(在本例为 simple_page
)作为函数端点(可以看作是 URL 的别名)的前缀。蓝图的名称不修改 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/' (HEAD, OPTIONS, GET) -> static>,
<Rule '/' (HEAD, OPTIONS, GET) -> simple_page.show>,
<Rule '/' (HEAD, OPTIONS, GET) -> simple_page.show>])
第一条很明显,是来自于app本身的用于静态文件的。后面两条是用于蓝图 simple_page
的 show 函数的。我们可以看到,它们的端点前缀都是蓝图的名称,并且使用一个点 .
来分隔。
蓝图的 URL 前缀可以修改:
app.register_blueprint(simple_page, url_prefix='/pages')
这样就会形成如下规则:
>>> app.url_map
Map([<Rule '/static/' (HEAD, OPTIONS, GET) -> static>,
<Rule '/pages/' (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
父蓝图指定的请求前函数等会为子蓝图触发。如果子蓝图没有可以处理异常的出错处理器,那么会尝试父蓝图的出错处理。
蓝图还可以用于提供资源。有时候,我们仅仅是为了使用一些资源而使用蓝图。
和普通app一样,蓝图一般都放在一个文件夹中。虽然多个蓝图可以共存于同一个文件夹中,但是最好不要这样做。
文件夹由 Blueprint
的第二个参数指定,通常为__name__
。这个参数指定与蓝图相对应的 Python 模块或包。如果这个参数指向的是实际的 Python 包 (一个文件夹),那么它就是资源文件夹。如果是一个模块,那么包含这个模块的包就是资源文件夹。可以通过 Blueprint.root_path
属性来查看蓝图的资源文件夹路径:
>>> simple_page.root_path
'/Users/username/TestProject/yourapplication'
可以使用 open_resource()
函数快速打开这个文件夹中的资源:
with simple_page.open_resource('相对路径') as f:
code = f.read()
Blueprint
的第三个参数是 static_folder
。这个参数用以指定该蓝图中静态文件所在的文件夹,它可以是一个绝对路径也可以是相对路径:
admin = Blueprint('admin', __name__, static_folder='xxx/static')
默认情况下,路径最右端的部分是在 URL 中暴露的部分。这可以通过 static_url_path
来改变。因为上例中的文件夹为名称是 static
,那么 URL 应该是蓝图的 url_prefix
加上 /static
。 如果蓝图注册前缀为 /admin
,那么静态文件 URL 就是 /admin/static
。
端点的名称是 blueprint_name.static
。你可以使用 url_for()
来生成(或者说反向解析)其 URL:
url_for('admin.static', filename='style.css')
但是,如果蓝图没有 url_prefix
,那么不可能访问蓝图的静态文件夹。 这是因为,在这个例子中,URL是 /static
,而 app 的 /static
路由优先级更高。与模板文件夹不同,如果 Flask 在 app 的静态文件夹中找不到目标文件,则不会去蓝图的静态文件夹中查找。
如果想使用蓝图来暴露模板,那么可以使用 Blueprint
的 template_folder
参数:
admin = Blueprint('admin', __name__, template_folder='templates')
对于静态文件,路径可以是绝对的或相对于蓝图的资源文件夹。
蓝图的模板文件夹,优先级低于 app 的模板文件夹。这样有助于在 app 中重载蓝图提供的模板。多个蓝图提供相同的相对路径时,第一个注册的优先。
假设:
蓝图位于 yourapplication/admin
中:
要渲染的模板是 'admin/index.html'
,template_folder
参数值为 templates
;
那么:
真正的模板文件路径为: yourapplication/admin/templates/admin/index.html
。
多出一个 admin
文件夹是为了避免模板被 app 模板文件夹中的 index.html
重载。
目录结构如下:
yourpackage/
blueprints/
admin/
templates/
admin/
index.html
__init__.py
这样,当你需要渲染模板的时候就可以使用 admin/index.html
来找到模板。 如果没有载入正确的模板,那么应该启用 EXPLAIN_TEMPLATE_LOADING
配置变量。 启用这个变量以后,每次调用 render_template
时, Flask 会打印出定位模板的步骤,方便调试。
如果你想从一个页面链接到另一个页面,可以和通常一样使用url_for()
函数,只是要把蓝图名称作为端点的前缀,并且用一个点 .
来分隔:
url_for('admin.index')
另外,如果在一个蓝图的视图函数或者被渲染的模板中需要链接同一个蓝图中的其他端点,那么使用相对重定向,只使用一个点使用为前缀:
url_for('.index')
如果当前请求被分配到 admin 蓝图端点时,上例会链接到 admin.index
。
蓝图像 Flask app 对象一样,也支持 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 的访问”时,app 无法知道应该运行哪个蓝图错误处理器。
如果想基于 URL 前缀执行不同的错误处理策略,那么可以在应用层使用 request
代理对象定义它们:
@app.errorhandler(404) # 注意:使用的是 app 对象
@app.errorhandler(405)
def _handle_api_error(ex):
if request.path.startswith('/api/'):
return jsonify(error=str(ex)), ex.code
else:
return ex