引入
正常情况下,我们简单的Flask程序都是单文件,把所有的视图函数写在一个文件里,比如说我有一个博客程序,前台需要首页、列表、详情等等。比如说我们创建一个app.py来实现这个功能。
#app.py 文件
from flask import Flask
app=Flask(__name__)
@app.route('/')
def index():
pass
@app.route('/list')
def list():
pass
@app.route('/detail')
def detail():
pass
if __name__=='__main__':
app.run()
假设内部逻辑完全实现,一个简单的博客系统就完成了,是不是很简洁。但是我们的博客可不是只有前台页面啊,我们的博主想要编辑博客,要进入后台进行处理:后台主页,编辑,创建,发布博客等等。行,既然有新需求了,那么我们就往app文件中加呗。如下:
#app.py 文件
from flask import Flask
app=Flask(__name__)
@app.route('/')
def index():
pass
@app.route('/list')
def list():
pass
@app.route('/detail')
def detail():
pass
# ---------------新加入的代码
@app.route('/')
def admin_home():
pass
@app.route('/new')
def new():
pass
@app.route('/edit')
def edit():
pass
@app.route('/publish')
def publish():
pass
if __name__=='__main__':
app.run()
假设新加入的代码逻辑也全部实现了,我们的博客系统就完成了。但是这只是理想化的情况,真正线上的博客可不是这么简单,业务都很复杂。
按照我们的设计,一个py文件中写入大量的路由,这样既不简洁,也不良好,将来维护代码会非常麻烦,所以有人就会想到了模块化的方法,把admin相关的代码移到一个admin.py中,那么就这么做一下。
# admin.py 文件
from app import app
@app.route('/')
def admin_home():
pass
@app.route('/new')
def new():
pass
@app.route('/edit')
def edit():
pass
@app.route('/publish')
def publish():
pass
这样写完后,发现启动程序,路由找不到,也就是说,使用python传统的模块化方式是行不通的,这时候就需要使用Flask内置的一个模块化处理的类,即蓝图(Blueprint),完美解决以上问题。那么蓝图究竟是什么?
什么是蓝图
简单来说,Blueprint 是一个存储操作方法的容器,这些操作在这个Blueprint 被注册到一个应用之后就可以被调用,Flask 可以通过Blueprint来组织URL以及处理请求。
Flask使用Blueprint让应用实现模块化,在Flask中,Blueprint具有如下属性:
- 一个应用可以具有多个Blueprint
- 可以将一个Blueprint注册到任何一个未使用的URL下比如 “/”、“/sample”或者子域名
- 在一个应用中,一个模块可以注册多次
- Blueprint可以单独具有自己的模板、静态文件或者其它的通用操作方法,它并不是必须要实现应用的视图和函数的
- 在一个应用初始化时,就应该要注册需要使用的Blueprint
但是一个Blueprint并不是一个完整的应用,它不能独立于应用运行,而必须要注册到某一个应用中。
蓝图的使用
使用蓝图大概需要三个步骤
- 创建蓝图对象
# 这里的两个参数为必选,至于为什么,稍后解释
admin = Blueprint('admin',__name__)
- 使用蓝图对象注册路由
@admin.route('/')
def admin_home():
pass
- 将蓝图注册到app上。注意:蓝图对象没有办法独立运行,必须将它注册到一个应用对象上才能生效
app.register_blueprint(admin)
完整demo
from flask import Flask,Blueprint
admin = Blueprint('admin',__name__)
app = Flask(__name__)
app.register_blueprint(admin)
@admin.route('/')
def admin_home():
pass
if __name__ == '__main__':
app.run()`
蓝图对象的初始化
首先来看一下蓝图对象的构造方法
def __init__(self, name, import_name, static_folder=None,
static_url_path=None, template_folder=None,
url_prefix=None, subdomain=None, url_defaults=None,
root_path=None):
pass
# name 蓝图对象的名字(标识)
# import_name 蓝图的资源文件夹,也就是蓝图的位置,该参数是一般是模块的名字
# static_folder 静态资源文件夹路径,相对路径
# static_url_path 静态文件url
# template_folder 模板文件路径
# url_prefix url前缀
# subdomain 子域名
# url_defaults 默认的路径参数和其对应的值的键值对,当其被设置后,本蓝图的所有视图函数便拥有该参数
# root_path 蓝图的资源文件夹的绝对路径,如果这个不是None,import_name的设置失效
由构造参数可知,为什么只有name和import_name是必传入的。
- url_prefix这个参数是干什么用的呢?
这个参数可以动态帮助我们构造url前缀。比如我们有下面的路由。
@app.route('/blog')
def index():
pass
@app.route('/blog/list')
def list():
pass
@app.route('/blog/detail')
def detail():
pass
从上面我们可以看出,所有的路由都是以blog开头的,若这样写代码的话,会增加代码的复杂性、降低可维护性。为了解决这个问题,我们可以在蓝图中定义动态的URL前缀。
blog=Blueprint('blog', __name__, url_prefix='/blog')
这样一来我们的路由可以写为
@app.route('/')
def index():
pass
@app.route('/list')
def list():
pass
@app.route('/detail')
def detail():
pass
- static_folder 注册静态路由
和应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。需要我们在 创建时指定 static_folder 参数。
blog = Blueprint("blog",__name__,static_folder='static_blog', url_prefix='/blog')
现在就可以使用/blog/static_blog/ 访问static_blog目录下的静态文件了。我们可以在创建蓝图对象时使用 static_url_path 来改变静态目录的路由。下面的示例将为 static_blog文件夹的路由设置为 /lib
blog = Blueprint("blog",__name__,static_folder='static_blog', static_url_path='/lib',url_prefix='/blog')
- template_folder 注册模板目录
蓝图对象默认的模板目录为系统的模版目录,可以在创建蓝图对象时使用 template_folder 关键字参数设置模板目录
blog = Blueprint('blog',__name__,template_folder='my_templates')
蓝图的运行机制-源码分析
我们已经知道蓝图怎么使用了,但是仅仅知道怎么使用是不够的,还是要了解下其背后的运行机制,才能更好的掌握蓝图,更好的使用它。
我们看一下app的 register_blueprint 方法,实际是调用了蓝图的register方法。
@setupmethod
def register_blueprint(self, blueprint, **options)
...
blueprint.register(self, options, first_registration)
继续看一下蓝图的register方法
def register(self, app, options, first_registration=False):
self._got_registered_once = True
state = self.make_setup_state(app, options, first_registration)
if self.has_static_folder:
state.add_url_rule(self.static_url_path + '/',
view_func=self.send_static_file,
endpoint='static')
for deferred in self.deferred_functions:
deferred(state)
这里有两个重要的地方,一个是state,一个是deferred_functions。
先看一下state是什么。
def make_setup_state(self, app, options, first_registration=False):
...
return BlueprintSetupState(self, app, options, first_registration)
通过make_setup_state这个方法我们发现state实际上是BlueprintSetupState的一个实例。
class BlueprintSetupState(object):
def __init__(self, blueprint, app, options, first_registration):
...
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
...
self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint),
view_func, defaults=defaults, **options)
发现BlueprintSetupState类中有一个add_url_rule方法,实际上是在调用flask app的路由操作,这个类个人理解是蓝图和flask连接的桥梁。
我们在看一下deferred_functions,通过源码发现recode和record_once在向deferred_functions里面添加函数。
def record(self, func):
...
self.deferred_functions.append(func)
def record_once(self, func):
...
return self.record(update_wrapper(wrapper, func))
record_once作用是这种添加只进行一次。通过源码分析我们找到record调用,发现是deferred_functions里面添加的是lambda函数,可以理解为视图函数和路由信息。
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
...
self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options))
我们理清了deferred_functions和state,回过头来在看register。
发现deferred_functions里的函数以 BlueprintSetupState 实例为参数,调用flask app的add_rule_url方法,实现路由的操作。
为什么deferred_functions叫延迟函数呢,我们通过源码看不难发现因为只有在register注册的时候才会真正调用flask app的add_rule_url方法,添加到url_map中。
总结一下整体流程:
- 当在应用对象上调用 route 装饰器注册路由时,这个操作将修改对象的url_map路由表
- 然而,蓝图对象根本没有路由表,当我们在蓝图对象上调用route装饰器注册路由时,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项
- 当执行应用对象的 register_blueprint() 方法时,应用对象调用蓝图的register方法,从蓝图对象的 defered_functions 列表中取出每一项,并以自身作为参数执行该匿名函数,即调用应用对象的 add_url_rule() 方法,这将真正的修改应用对象的路由表。