Flask中的蓝图

引入

正常情况下,我们简单的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 + '/<path:filename>',
                               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() 方法,这将真正的修改应用对象的路由表。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350