一. Flask-SQLAlchemy 初始化源码
在文件中定义了工厂函数 create_app :
def create_app(config):
""" 可以根据传入的 config 名称,加载不同的配置
"""
app = Flask(__name__)
app.config.from_object(configs.get(config))
# SQLAlchemy 的初始化方式改为使用 init_app
db.init_app(app)
如上所示的最后一行代码,db 对象一般是在models.py
文件中定义的 SQLAlchemy 类的实例,db = SQLAlchemy()
。这里将应用对象作为参数调用实例的 init_app
方法进行初始化。
到现在为止我们只知道初始化,但并不知道实际发生了什么。
我们看下 SQLAlchemy 这个类的源码,首先它是来自 flask_sqlalchemy.__init__
模块中:
cd /usr/local/lib/python3.8/dist-packages/flask_sqlalchemy
sudo vim __init__.py
class SQLAlchemy(object):
"""This class is used to control the SQLAlchemy integration to one
or more Flask applications. Depending on how you initialize the
object it is usable right away or will attach as needed to a
Flask application.
There are two usage modes which work very similarly. One is binding
the instance to a very specific Flask application::
app = Flask(__name__)
db = SQLAlchemy(app)
因为在创建该类的实例的时候,没有提供参数,我们就直接看 init_app 方法的源码:
如上图所示,init_app 方法只需要提供一个 app 参数,也就是应用对象。
在第 847 行有一个判断,要求 app.config 这个类字典对象中必须要有 SQLALCHEMY_DATABASE_URI
或者 SQLALCHEMY_BINDS
这两个配置项中的一个,不然就会抛出一个警告信息:默认使用 SQLite3 作为数据库。
我们已经在配置文件中定义了 SQLALCHEMY_DATABASE_URI ,所以不会出现这个警告。
下面就是一堆给 app.config 字典对象添加默认键值对的操作。
继续向下查看源码,如下图所示第 884 行是最重要的:
应用对象在初始化的时候,定义了一个 extensions
属性,属性值是空字典,这是用来存放各种扩展对象的地方。
在执行 db.init_app(app)
时,就会将 ‘sqlalchemy’ 字符串作为 key ,_SQLAlchemyState
类的实例作为 value ,添加到 app.extensions 字典中。
我们看下这个 _SQLAlchemyState 类的源码,它也在 flask_sqlalchemy.__init__
模块中:
如上图所示,初始化此类时提供的参数就是 SQLAlchemy 类的实例,也就是说 _SQLAlchemyState
类的实例的 db 属性值是 SQLAlchemy 类的实例,也就是 db 对象。
总结:
Flask-SQLAlchemy 库中的 SQLAlchemy 类的实例是 db 变量,调用它的 init_app 方法初始化,主要就是为应用对象的配置增加一些默认值,以及将 ‘sqlalchemy’ 作为键将 db 添加到 extensions 字典中;
二、蓝图源码分析
蓝图的英文名字是 Blueprint ,是 Flask 提供的将应用组件化的一种机制。对一个中大型网站来说,有很多个板块。
每个板块下面都会有多个页面。我们知道,Flask 应用程序要实例化一个应用对象 app 来管理 Flask 应用,而 Blueprint 可以让你将板块注册为 app 下的一个子 app。
这个子 app,也就是我们注册的蓝图,它的使用方法基本上和 app 一样的。
比如,我们注册一个课程板块的蓝图:
from flask import Blueprint
course = Blueprint('course', __name__, url_prefix='/courses')
然后调用 app 的 register_blueprint
将这个 course Blueprint 注册到 app:
def register_blueprints(app):
"""注册蓝图的函数"""
app.register_blueprint(course)
这样在注册路由时,比如课程首页的路由就可以这样写:
@course.route('/')
def courses_index():
return render_template('courses.html')
现在可以在 create_app 中调用注册蓝图的函数来注册蓝图:
def create_app(config):
""" App 工厂"""
app = Flask(__name__)
app.config.from_object(configs.get(config))
db.init_app(app)
# 注册 Blueprint
app.register_blueprint(api)
#register_blueprints(app)
return app
定义api.所对应的url
定义了所有 API 对应的 URL
"""
from flask import Blueprint
from rmon.views.index import IndexView
api = Blueprint('api', __name__)
# 蓝图的 add_url_rule 方法定义路由及其视图函数
# 第一个参数为路径,第二个参数为视图函数
# 自定义 as_view 的参数作为视图函数名
# 例如浏览器使用 GET 方法访问主页时,由 IndexView 类中的 get 方法处理请求
api.add_url_rule('/', view_func=IndexView.as_view('index'))
下面来研究一下源码,弄清楚创建蓝图时的参数在蓝图内部是怎么使用的。
Blueprint 类定义在 flask.blueprints 模块中:
cd /usr/local/lib/python3.8/dist-packages/flask
sudo vim blueprints.py
class Blueprint(Scaffold):
"""Represents a blueprint, a collection of routes and other
app-related functions that can be registered on a real application
later.
A blueprint is an object that allows defining application functions
without requiring an application object ahead of time. It uses the
same decorators as :class:`~flask.Flask`, but defers the need for an
application by recording them for later registration.
看下它的初始化方法:
在我们的代码中,涉及到的参数有三个,如上图所示蓝色框中。它们分别赋值给蓝图对象的同名属性:第 184 、185 行设置了 name 和 url_prefix 属性,而 import_name 是由父类设置的。第 181 行调用了父类的初始化方法,此类定义在 flask.helpers 模块中:
定义好蓝图之后,就可以使用蓝图的路由装饰器来创建视图函数(也叫路由函数)了,我们看下此方法的源码:
与应用对象 app 的 route 方法类似,都是调用自身的 add_url_rule
方法添加路由规则。我们以 front 蓝图的 index 视图函数为例,参数 rule 是相对路径字符串 '/'
,options 是空字典,第 278 行设置变量 endpoint 的值为视图函数名 'index'
。
调用 add_url_rule 方法就是上图 284 行的方法。第 288 行判断 endpoint 变量字符串中是否有点号,如果有就抛出异常。同理第 290 行判断视图函数的 __name__
属性值是否带点号。
最后 294 行调用自身的 record 方法,参数是一个匿名函数。先不管这个匿名函数,我们看下 record 方法:
如上图所示,这里调用了蓝图的 deferred_functions
的 append 方法,deferred_functions
属性是一个空列表,它是在蓝图初始化时定义的。
也就是说,在创建蓝图对象之后,会执行路由装饰器 route 方法把每一个视图函数对应生成的匿名函数放到蓝图的 deferred_functions
列表里。
注册蓝图流程分析
启动应用程序后,创建应用对象,然后执行注册蓝图的函数。
在app.py 文件中注册了蓝图,也就是调用应用对象的 register_blueprint 方法:
def register_blueprints(app):
"""注册蓝图的函数"""
from .handlers import front, course, admin
app.register_blueprint(front)
app.register_blueprint(course)
app.register_blueprint(admin)
我们看下此方法的源码:
删掉了源码中的一些注释以便于讲解。如上图所示,我们在调用此方法时只提供了一个蓝图对象作为 blueprint 这个参数的值。
第 1143 行有一个判断,blueprint 就是蓝图对象,blueprint.name
是我们定义蓝图时提供的第一个参数字符串。
以 front 这个蓝图为例,blueprint.name 的值就是 ‘front’ 。而 self.blueprints
是应用对象 app 在初始化时定义的属性,属性值是空字典。所以这个语句为 False ,继续执行 1150 行下面的内容。第 1151 行将蓝图名字作为 key ,蓝图作为 value 向 app 的 blueprints 字典中添加一组键值对。
第 1152 行有个 _blueprint_order
属性,它也是在应用对象初始化时定义的属性,属性值是空列表:
回到 register_blueprint 方法中,最后一行 1155 行代码调用了蓝图自身的 register 方法,我们再回到定义蓝图的 flask.blueprints 模块中找到这个方法的源码:
如上图所示,第 246 行通过 make_setup_state
方法创建了一个当前模块中的 BlueprintSetupState
类的实例并赋值给 state 变量。
这里我们主要关注上图所示最后两行代码,循环调用蓝图的 deferred_functions
列表中的匿名函数,这个匿名函数就是蓝图的 add_url_rule
方法中的那个:
调用此匿名函数时,参数 s 就是 state ,也就是当前模块中的 BlueprintSetupState
类的实例,在匿名函数内部调用该实例的 add_url_rule 方法:
如上图所示第 70 行的 self.url_prefix
是来自与蓝图的同名属性,例如 front 对应的就是 None ,course 对应的就是 ‘/courses’ 。
第 65 行的参数 rule 就是相对路径,举两个例子:front 蓝图的 index 视图函数的 rule 就是 ‘/’ ,在 add_url_rule
内部的值不变;admin 的 index 视图函数的 rule 也是 ‘/’ ,它在 add_url_rule 内部第 70 行被处理,加上了 url_prefix ,最后变成 '/admin'
。
再举一个明显的例子,course 蓝图中假定有一个视图函数,它被 @course.route('/create_new')
装饰器装饰,那么在这里 rule 参数的值就是 '/create_new'
,在函数内部处理后的值就是 '/courses/create_new'
。也就是说,所有的 course 蓝图下的视图函数在这里被处理时,rule 都是以 '/courses'
开头的绝对路径。
参数 view_func
是视图函数,第 77 行调用 _endpoint_from_view_func
获取视图函数的名字字符串并赋值给 endpoint 变量。
第 78 ~ 80 行处理变量 defaults ,它是蓝图在初始化时设置的默认属性,属性值一直是空字典,到这里依然是。
关键就在第 81 行,调用 self.app
的 add_url_rule 方法,self.app 就是应用对象,它的 add_url_rule 方法我们在前面的实验中已经学习过,注意第二个参数是用点号将蓝图的 name 属性与 endpoint 连起来得到的字符串,举个首页的例子:
关于应用对象的 add_url_rule 方法,这里再描述一下:
以首页为例,参数 rule 是 URL 的绝对路径 ‘/’ ,endpoint 是蓝图 name 属性值 + 视图函数名字的字符串 'front.index'
,view_func 是定义在 /handlers/front.py
文件中的视图函数。
下面仅对此方法的关键代码进行说明:
如上图所示第 1275 行的 rule 是根据参数 rule 创建的 werkzeug.routing.Rule
类的实例。注意这里的 options 关键字参数不是空字典啦,它里面至少有一组键值对,key 是 ‘endpoint’ ,value 是 ‘front.index’ 。相关代码如下,就不贴图了:
options["endpoint"] = endpoint
在对 werkzeug.routing.Rule
类进行实例化时,将实例的 rule 属性值设为 ‘/’ ,将实例的 endpoint 属性值设为 ‘front.index’ 。
第 1278 行,self.url_map
是一个 Map 对象,将 rule 添加到其 _rules 属性中,这个属性是一个列表对象。
第 1286 行,self.view_functions
是字典对象,将 endpoint 'front.index'
作为 key 、front 蓝图下的 index 视图函数作为 value 添加到字典中。
flask 中浏览器请求后端视图函数的过程
应用程序启动后,服务器收到浏览器的请求,处理 URL 后得到绝对路径 ‘/’ ,到 app.url_map
中找到 rule 属性值为 ‘/’ 的 Rule 对象,然后获取它的 endpoint 属性值 'front.index'
,再到 app.view_functions 字典中找到 key 为 ‘front.index’ 对应的 value ,也就是视图函数,最后调用视图函数处理请求。
思路整理
- 创建蓝图;
- 定义视图函数,视图函数的 route 装饰器会创建对应的匿名函数放到蓝图的 deferred_functions 列表中;
- 应用对象 app 调用自身的
register_blueprint
方法注册蓝图; - 在注册蓝图的方法内部调用蓝图自身的
register
方法; - 蓝图的 register 方法调用蓝图的
deferred_functions
列表中的匿名函数; - 匿名函数再通过一些流程调用应用对象 app 的 add_url_rule 方法将视图函数、路径、endpoint 这些关键点存到 Map 和字典对象中;
- 请求进来后,提取路由中的绝对路径,根据路径找到 endpoint ,根据 endpoint 再找到视图函数,最后调用视图函数处理请求。
最后的结果就是一个大的项目根据业务分成几类,每一类的路由归到一个蓝图下,根据蓝图的 url_prefix
路由前缀来区分。