前言

学习一样东西,要先知其然,然后知其所以然。

这次,我们看看Flask Web框架的源码。我会以Flask 0.1的源码为例,把重点放在Flask如何处理请求上,看一看从一个请求到来到返回响应都经过了什么过程。

你可能会问,为什么以Flask 0.1为例啊,那都是好几年前的一坨老代码了?老,并不代表没有用。相反,Flask 0.1的源码设计极为精妙,包含了Flask的主干部分,整个项目只有一个文件,六百行左右,分析起来也简单,有利于我们了解整个Flask的脉络。你可以从这里来获取Flask 0.1的源码。

Flask中定义的几个的类和函数

在Flask 0.1的源码中,一共定义了五个类:

  1. RequestResponse, 它们分别是Flask的请求和响应对象,分别继承自Werkzeug中的请求和响应类
  2. _RequestContext,请求上下文类。它包含了所有请求的相关信息。包括程序实例app,url匹配器,请求对象,session对象,g对象以及用于记录闪现的消息的flashes
  3. _RequestGlobals,使用该类创建g对象,这个对象内没有任何的属性,你可以给该类的实例(即g)绑定任何的全局属性。
  4. Flask,它是整个Flask框架的中心类,它实现了WSGI程序用于处理请求和响应,并且,它是整个所有视图函数、模板配置、URL规则的中心注册处。

另外,Flask中还定义了一些函数:如render_templateurl_forflashget_flashed_messages等,相信大家都知道这些函数的作用,我就不在赘述。下面我们着重看看Flask类。

Flask中本地上下文

在flask.py文件的最后,定义了几个全局对象:

_request_ctx_stack = LocalStack()
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)

其中,_request_ctx_stack是Flask的请求上下文堆栈,它的栈顶即是当前请求上下文对象的实例,当一个请求到来时,Flask会将一个请求上下文对象推入这个堆栈以便在程序中使用。current_apprequestsessiong通过代理的方式从上下文堆栈中获取到所需要的值。如果你还不清楚LocalStackLocalProxy,可以参见什么时Werkzeug

Flask类

下面,我们重点看一下Flask类是如何定义的。

从Flask类开头和__init__看起

在开始处,我们会看到Flask将RequestResponse分别赋值给了request_classresponse_class.

request_class = Request
response_class = Response

Flask并没有在程序中直接使用RequestResponse来生成请求和响应对象,而是通过调用request_classresponse_class来生成,这就给我们自定义请求和响应类提供了方便。你可以通过继承Flask中的请求和响应类来构建自己的请求和响应类,并将它们赋值给request_classresponse_class即可。

在下面:

static_path = '/static'
secret_key = None
session_cookie_name = 'session'

我们可以看到在这里Flask定义了静态资源的目录,密钥以及cookie的名称,当然,这些你也可以手动进行修改。

下面,我们看一看__init__

def __init__(self, package_name):
self.debug = False # 是否开启调试 self.package_name = package_name self.root_path = _get_package_path(self.package_name) # 程序的根目录 self.view_functions = {} # 用于保存注册的视图函数 self.error_handlers = {} # 保存注册的错误处理函数 self.before_request_funcs = [] # 保存请求开始时前调用的函数 self.after_request_funcs = [] # 保存请求完成后调用的函数 self.url_map = Map() # 保存路由规则

可以看到,我们用字典来保存注册的视图函数和错误处理函数,以及用列表保存请求前后要掉用的函数。其中用一个Map对象url_map来保存我们对URL进行处理的路由规则,其中每个路由规则为一个Rule对象,我们会在下文看到。另外,__init__中还定义的用于保存模板函数的属性,以及Jinja2环境对象,这里不在一一列出。

用于注册视图函数的装饰器

众所周知,我们可以使用下面的方式来注册视图函数:

@app.route('/')
def index():
return 'Hello World'

我们看一下这个装饰器是如何实现的:

def route(self, rule, **options):
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator

route中,会对我们传入的视图函数进行包装,首先调用Flask中的add_url_rule方法,然后以函数名为键,将视图函数保存在__init__中定义的用于保存视图函数的view_functions字典中。

下面,我们看看在add_url_rule(rule, f.__name__, **options)内部发生了什么:

def add_url_rule(self, rule, endpoint, **options):
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',)) # 默认监听GET方法
self.url_map.add(Rule(rule, **options))

add_url_rule中,Flask首先将以'endpoint'为键,将端点值放入options中。如果options没有'methods'键,Flask会在这里给我们添加一个默认的GET方法,也就是说,当我们直接使用@app.route('/'),而不传入监听的方法时,Flask会默认监听GET方法。最后,Flask以当前的ruleoptions创建一个Rule对象放入到url_map中,为我们的程序新增了一条路由规则。

另外,除了route装饰器外,Flask中还有还提供了用于注册错误函数、请求前调用的函数、请求后调用的函数等的装饰器,这些装饰器和route装饰器基本相同,只是没有添加路由规则这个功能。例如请求处理前调用的函数的的装饰器:

def before_request(self, f):
self.before_request_funcs.append(f)
return f

Flask请求响应流程

Flask中定义了wsgi_app(self, environ, start_response)方法作为WSGI的程序,它并没有写死在__call__方法中,因此可以为其添加中间件。当请求到来时,WSGI服务器会调用此方法,并将请求的参数和用于发起响应的函数作为参数传递给它。

def wsgi_app(self, environ, start_response):
with self.request_context(environ):
rv = self.preprocess_request() # 预处理请求
if rv is None:
rv = self.dispatch_request() # 请求分发
response = self.make_response(rv) # 生成响应
response = self.process_response(response) # 响应处理
return response(environ, start_response)

Flask在with语句下执行相关操作,这会触发_RequestContext中的__enter__方法,从而推送请求上下文到堆栈中。在with中,Flask先通过preprocess_request()预处理请求,在preprocess_request()中调用所有在beforce_request()装饰器中注册的请求前要调用的函数。随后,Flask使用dispatch_request()来进行请求分发,获得视图函数的返回值或是错误处理器的返回值。然后Falsk将请求分发时获得的返回值传给make_response()方法来生成一个响应对象,接下来,Flask在process_response()方法中调用所有在after_request()装饰器中注册的请求完成后要调用的函数。最后,通过response来发起一个响应,这会自动调用start_response方法来发起响应并将响应的值返回给WSGI服务器。

预处理请求

def preprocess_request(self):
for func in self.before_request_funcs:
rv = func()
if rv is not None:
return rv

上面的函数会在实际的请求分发之前调用,而且将会调用每一个使用before_request()装饰的函数。如果其中某一个函数返回一个值,这个值将会作为视图返回值处理并停止进一步的请求处理。

请求分发

def dispatch_request(self):
try:
endpoint, values = self.match_request()
return self.view_functions[endpoint](**values)
except HTTPException, e:
handler = self.error_handlers.get(e.code)
if handler is None:
return e
return handler(e)
except Exception, e:
handler = self.error_handlers.get(500)
if self.debug or handler is None:
raise
return handler(e)

在上面的方法中,Flask对URL进行匹配获取端点值和参数,然后调用相应的视图函数并将视图函数的返回值返回,或者返回相应的错误处理器的返回值。这里的返回值不一定是响应对象,比如我们可以在视图函数中返回一个字符串或者是使用render_template()渲染好的模板,所以,为了能够将返回值转换成合适的对象,我们需要make_response()方法来生成响应

生成响应

def make_response(self, rv):
"""
rv允许的类型如下所示:
======================= ===============================================
response_class 这个对象将被直接返回
str 使用这个字符串作为主体创建一个请求对象
unicode 将这个字符串进行utf-8编码后作为主体创建一个请求对象
tuple 使用这个元组的内容作为参数创建一个请求对象
a WSGI function 这个函数将作为WSGI程序调用并缓存为响应对象
======================= ===============================================
:param rv: 视图函数返回值
"""
if isinstance(rv, self.response_class):
return rv
if isinstance(rv, basestring):
return self.response_class(rv)
if isinstance(rv, tuple):
return self.response_class(*rv)
return self.response_class.force_type(rv, request.environ)

在上面的方法中,也印证了我们上面所说的请求分发中视图函数的返回值不一定是请求对象这一点。所以,我们在make_response方法中对请求分发中获取的返回值的类型进行判断,通过不同的方式来创建真正的响应对象并返回。

响应处理

def process_response(self, response):
session = _request_ctx_stack.top.session
if session is not None:
self.save_session(session, response)
for handler in self.after_request_funcs:
response = handler(response)
return response

响应处理和预处理请求类似,都会循环调用所有注册的请求后调用的函数来对响应对象response进行处理,不过在此之前会先将session添加到响应对象中。

返回响应

我们Flask中的响应对象会继承自Werkzeug中的Response对象。Response的实例可以根据传入的参数,来发起一个特定的响应。你可以认为Response是你可以创建的另一个标准的WSGI应用,这个应用可以根据你传入的参数,来帮你做发起响应这件事。例如下面一个简易的WSGI程序:

def application(environ, start_response):
request = Request(environ)
response = Response("Hello %s!" % request.args.get('name', 'World!'))
return response(environ, start_response)

好了,到此,Flask的一次请求就处理完了。不难发现,在Flask中,对Werzeug这个工具库是很依赖的,从请求处理,路由匹配,到发起请求,都可见到Werkzeug的身影。

总结

请求响应类,请求上下文类,全局对象类,核心类Flask

Flask中,保存有视图函数、错误处理函数、路由规则,可以处理请求

请求处理流程:预处理请求、请求分发、生成响应、返回响应


参考:

  • 《Flask Web开发实战》

Flask源码浅析的更多相关文章

  1. flask 源码浅析(flask 如何处理请求(多线程,多进程,IO多路复用))

    2018-04-04 13:09:47 lucky404 阅读数 5724更多 分类专栏: python   版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接 ...

  2. 【深入浅出jQuery】源码浅析--整体架构

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  3. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  4. flask源码分析

    本flask源码分析不间断更新 而且我分析的源码全是我个人觉得是很beautiful的 1 flask-login 1.1 flask.ext.login.login_required(func),下 ...

  5. Flask源码学习—config配置管理

    自己用Flask做了一个博客(www.hbnnlove.sinaapp.com),之前苦于没有对源码解析的文档,只能自己硬着头皮看.现在我把我自己学习Flask源码的收获写出来,也希望能给后续要学习F ...

  6. Struts2源码浅析-ConfigurationProvider

    ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...

  7. (转)【深入浅出jQuery】源码浅析2--奇技淫巧

    [深入浅出jQuery]源码浅析2--奇技淫巧 http://www.cnblogs.com/coco1s/p/5303041.html

  8. HashSet其实就那么一回事儿之源码浅析

    上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap,  本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...

  9. Android 手势识别类 ( 三 ) GestureDetector 源码浅析

    前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...

随机推荐

  1. myeclipse中从svn下载的web工程,到工作空间却显示成Java工程

    转载自:https://blog.csdn.net/u011217058/article/details/57970587 右键工程,properties-> Project Facets-&g ...

  2. Hyper-V虚拟机上安装Ubuntu16.04/Ubuntu18.04.2LTS,搭建GitLab

    我的电脑系统是win10,内存8g如下 一开始是装的Ubuntu18.04.2LTS, gitlab-ce_12.1.3-ce.0_amd64.deb,每次能够安装成功,但是修改完ip后,运行gitl ...

  3. 保存localStorage并访问

    将用户的输入保存至localStorage对象的属性中,这些属性在再次访问时还会继续保持在原位置. 如果你在浏览器中按照fil://URL的方式直接打开本地文件,则文法在某些浏览器中使用存储功能(比如 ...

  4. [Inno Setup]写入注册表时32位系统和64位系统的路由

    昨天下午组内一位同事跟说,他想在Inno Setup的安装包中写入一个注册表.目标位置是HKLM:\Software\下面创建自己的注册表项.然后说尝试了好几次都不行, 但是往HKCU下面写入却是OK ...

  5. Of efficiency and methodology

    There are only too many articles and books which pertains to the discussion of efficiency and method ...

  6. 转载 | textarea 在浏览器中固定大小和禁止拖动

    HTML 标签 textarea 在大部分浏览器中只要指定行(rows)和列(cols)属性,就可以规定 textarea 的尺寸,大小就不会改变,不过更好的办法是使用 CSS 的 height 和 ...

  7. 史上最全面的SignalR系列教程-3、SignalR 实现推送功能-集线器类实现方式

    1.概述 通过前两篇 史上最全面的SignalR系列教程-1.认识SignalR 史上最全面的SignalR系列教程-2.SignalR 实现推送功能-永久连接类实现方式 文章对SignalR的介绍, ...

  8. 浅谈IDEA集成SSM框架(SpringMVC+Spring+MyBatis)

    前言 学习完MyBatis,Spring,SpringMVC之后,我们需要做的就是将这三者联系起来,Spring实现业务对象管理,Spring MVC负责请求的转发和视图管理, MyBatis作为数据 ...

  9. linux环境下测试环境搭建

    一.linux环境下测试环境搭建过程简述: 1.前端后台代码未分离情况下: 主要步骤为:安装jdk,安装mysql,安装tomcat,将项目代码部署到tomcat/webapps/下. 2.前端后台代 ...

  10. adb 最常用最简单的命令-install/push/pull 使用

    以vivo测试机为例1.网上下载adb工具包,安装---网上有教程2.手机连接电脑后,进入手机设置--更多设置---开发者选项,打开开发者选项和USB调试: (不同手机开发者选项进入方式不同)3.打开 ...