微框架之“微”
Flask 强调自己是一个用于 Web 开发的微框架。我们知道,开发 Web 应用主要的工作,就是对一个 Web 请求,接收其请求数据(输入),根据业务逻辑进行处理,然后返回相应的响应结果(输出)。Flask 微框架的“微”字,体现在它专注于上面这个流程的两端,即处理输入数据和生成输出数据。至于中间如何进行处理,那是开发人员的事情,Flask 并未提供便利。
基本的开发思路
主观感受上,我们更多的是将 Flask 当做一个库而不是框架在使用的。这赋予了我们更大的灵活性,得到输入数据后,如何验证,如何处理,使用什么技术处理,我们具有极大的自由。只要最终我们能够用上 Flask 提供的输出工具把结果抛回去就可以了。
Flask 没有强加给开发人员诸如 MVC 或 MTV 这样的设计模式,很多时候,响应一个请求的代码从较高的层面来看,仅仅是编写一个 Flask 称为视图的函数:
@app.route('/path/to/here') # 0. 用修饰器的形式描述路由规则
def perform(**kwargs):
"""请求处理函数"""
# 1. 围绕 request 对象获取输入数据
params = fetch_params(request, kwargs)
# 2. 根据业务需要进行必要的处理
result = deal_with(params)
# 3. 使用 Flask 提供的机制,通常是 render_template() 或 jsonify() 生成响应结果
return create_response(result)
Flask 没有所谓模型层的概念,模板层也不是必需的(虽然从包的依赖上看对 Jinja 的依赖是必需的),但是完全可以使用其它模板技术甚至完全不使用模板。开发中需要关注的,就是获取输入数据,进行必要的处理,然后输出对应的信息。
笔者认为,恰恰是如此简单直观的设计哲学,才让 Flask 深受欢迎。这篇文章将从这几个简单的步骤展开,记述笔者自己对如何使用 Flask 的理解。
输入数据的获取
从技术的层面,一个请求的输入数据有几个层面的含义:
它是遵循 HTTP 规范(通常是 HTTP 1.1)及一些扩展(例如我们可能自行约定特殊的 header 字段)的请求报文。这是背景知识,我们需要知道 HTTP 请求大概是怎样的形态。
在 Flask 应用程序中通常表现为“全局”对象
request
,它是 flask.Request 类的实例。这是对 HTTP 请求报文的抽象和封装,它提供了设计良好的属性和方法,来表现一个 HTTP 请求。
注意这个全局是打了引号的。后面我们会进行讨论,在这里可以粗略的理解在处理单个请求的过程中这个对象就像是全局变量一样。
通常来说,我们理解的输入数据应该更加扁平直观一些,它们应该是原始数据类型的某种较简单(列表、字典)的组合,这些数据通常是从 request
对象中提取的。一般来说,存在几个来源:
请求的路径可能本身被当做输入参数一部分,Flask 提供了一些便利,用于从请求路径中提取变量。详情请参考 Flask 快速上手指南 和 Werkzeug URL 路由模块文档。
请求的
QUERY_STRING
部分,可以通过request.args
获得。请求以表单形式提交的数据(即以
application/x-www-form-urlencoded
MIME Type 提交的报文),可以通过request.form
获得。request.args
和request.form
是MultiDict
的实例,通常可以当做普通的字典使用,不过可以用于处理同名属性出现多次的问题。request
还提供了其它属性提供开发人员可能感兴趣的输入信息,包括request.data
、request.headers
、request.files
和request.cookies
等。
Flask 并未提供对数据的验证。这是程序员需要也必需自行处理的,因为来自网络的数据是不安全的。通常有如下做法:
从路由规则提取的来自请求路径的信息,一般来说比较简单,手写简单的验证即可。
对于
request.args
和request.form
,简单但强大的验证数据的方法是通过Flask-WTF
扩展,得益于WTForms
库提供的验证机制,可以灵活的进行数据验证。其它输入来源得到的数据,例如
request.data
得到的 JSON 文本或 XML 文本,或者手工处理,或者借助注入 JSNON Schema 或 XML Schema 等技术进行验证。
结束响应和生成输出数据
当处理输入的过程成功得以执行,或者在处理数据的过程中产生了无法继续处理的错误,我们需要结束响应过程,并根据情况生成输出数据。
虽然大多数情况下,我们响应请求输出数据的时候,只需要生成报文即可,但是我们需要知道,Web 响应包含三个部分:状态码、报头和报文。Flask 提供了必要的手段让我们可以指定状态码并在报头注入必要的信息。
从 HTTP 的角度看,响应有三种类型:
具有 HTTP 2xx 状态码,从 HTTP 的角度这是成功的响应。
具有 HTTP 3xx 状态码,从 HTTP 的角度这是特殊的用于跳转到新的 URL 地址,需要浏览器或者客户端重新发起请求的响应。
具有 HTTP 4xx 或 5xx 状态码,从 HTTP 的角度这返回了错误信息,4xx 系列一般来说是请求存在问题,5xx 系列一般来说是服务器存在问题。
从输出的内容形态来看,通常有:
适合人们阅读的内容,通常是 HTML,通过浏览器进行渲染。一般来说,会需要通过
render_template()
函数结合模板文件的处理结果进行渲染,当内容较为简单时,直接以字符串形式生成 HTML 也是可行的。适合机器阅读的内容,常用于接口服务的开发,例如通过调用
jsonify()
函数返回 JSON 格式的数据。无内容。特定的状态码如
204 No Content
不得包含响应内容。这通常也是用于接口服务的开发。
在结束响应方面,存在两种方式:
自然的方式,即请求处理函数以
return
语句返回必要的信息,可以是一个flask.Response
类的实例,也可以是用于构建这样实例的参数。中途跳出的方式,即以抛出异常(一般来说是对应了不同状态码的特定的异常)的方式(包括调用 Flask 提供的
abort()
函数),来提早结束处理。
可以看到,Flask 在结束响应和生成输出方面考虑了不少情况,提供了许多的便利。然而,仍然存在一处缺失:就是当我们以特定的异常或调用 abort()
函数跳出处理时,它假定错误信息是以 HTML 的方式提供的,当我们遵循特定的约定开发接口服务器时,需要继承特定的异常重写必要的报头和报文。
微框架之“框架”
虽然许多时候以及 Flask 的许多部分我们都是当做库来使用的,然而 Flask 毕竟是一个框架。简单来说,这意味着代码的组织必然受到框架内在特点的影响。
虽然从 0.7 版开始,Flask 就支持了基于类形态的视图机制,但是笔者更倾向于以函数的方式编写视图,也就是采用更传统的过程式形态。这让代码保持简单,易于阅读和维护。
使用修饰器改变响应处理行为
使用类的形态带来的好处是可以开发人员编写一个或者多个视图类,它们具有一定的行为,再供给它们的子类使用。这些行为主要的作用是在执行真正的响应代码之前和/或之后进行必要的处理。如果缺少必要的手段,采用过程形态编写程序,这样的通用代码如果分散在各个视图函数中,就显得冗余了。
幸运的是,得益于 Python 的修饰器机制,我们同样可以简单的在视图函数得以执行之前并在执行之后,进行必要的预处理和/或后期处理,而无需编写冗余的代码。
举个例子,对于要求用户已经登录方可执行的响应处理,如果以类形态编程,可能会表现为一个父类,在真正分发请求之间检查用户是否已经登录,子类中就无需再考虑相应的问题,用户未登录时请求不会被分发到相应的方法上。
以过程式形态来编写,将这样的判断写到每一个函数里,显然是冗余的。因此,诸如扩展 Flask-Login
是通过提供名为 login_required
的修饰器来进行的,代码看起来类似:
@app.route('/path/to/here')
@login_required
def perform(**kwarg):
params = fetch_params(request, kwargs)
result = deal_with(params)
return create_response(result)
这样,要求用户已经登录的行为从代码上看简化为一行修饰器的使用,由于它与函数定义的位置相当近,实际上代码的可读性是更高的,阅读这段代码时,脑海中更容易强化要求用户已经登录的约定。
对于今天较先进的程序设计语言提供的修饰器(Decorator)或者标注(Annotation)这样的技术,笔者的理解是这是一种允许开发人员定制的超级语法糖,它能够在编译时(对编译型语言)或模块导入时(对于解释型语言)或多或少的修改源代码。好的实现,可以让源代码具有很高的易读性和可维护性的同时,具备更强大的功能。
在 Python 中,修饰器是一个便利的拦截和修改调用参数和返回值的机制。因此要深入驾驭 Flask,了解如何编写修饰器是有必要的。
使用 Blueprint 切分视图模块
以类形态组织代码,天然具有良好的模块隔离。以过程式形态,我们也可以将逻辑上相关的响应处理函数整理归并到不同的 Python 模块中。更进一步,我们还可以使用 Flask 提供的 Blueprint 概念,Blueprint 在 API 与全局的 Flask 应用程序类似,使用 Blueprint 在实践中还具有一些额外的好处,例如可以较为方便的调整一个 Blueprint 中所有响应处理函数的路由规则,以及避免对全局的 Flask
对象循环依赖等。
保持面向对象的设计思想
即便是过程式形态组织代码,我们整体的思路依然是面向对象的。每个模块中的响应处理函数可以认为是当前运行中的 Flask
应用程序实例的方法。我们可以通过 flask.current_app
对象获得应用程序上下文中的这个实例。此外,可以通过 flask.g
来存储实例属性。通过这样的思考,我们仍然可以保持面向对象的程序设计思想。
使用回调和信号机制
Flask 具有请求上下文的概念,提供了必要的回调机制,回调有三类:
before_request
回调,当收到请求时被调用,用于请求数据的预处理,必要时也可以返回响应对象以抑制响应处理函数的执行。after_request
回调,当请求处理正常结束时被调用(如果处理过程抛出了未被处理的异常则不会被调用),用于响应结果的后处理。它们接受一个响应对象作为参数,并应该返回一个响应对象。teardown_request
回调,在每个请求处理结束时被调用,通常用于清理或者回收资源。
可以看到 before_request
回调和 after_request
回调也可以用于请求处理函数的预处理和后处理。teardown_request
回调的使用场景较为特殊,之后我们会更进一步说明。
每一类回调均可以设置多个,调用时,before_request
回调按照注册顺序进行,after_request
回调和 teardown_request
回调按照注册的逆序执行。
除了回调之外,Flask 也提供了更加灵活的信号机制,除了能够处理的时机比回调更多,也允许开发人员自定义信号。
WSGI 应用程序
Flask 是遵循 WSGI 规范的应用程序实现。WSGI 是许多 Python Web 框架——包括著名的 Django 框架——都共同遵循的规范。这意味着,基于 Flask 编写的应用程序,可以运行在不同的 WSGI 服务器实现上,并且可以结合各种 WSGI 中间件使用。
“全局”变量
许多 WSGI 服务器的实现,例如 uWSGI 和 Gunicorn 均支持多种不同的并发实现:多进程、多线程以及协程等。特别的,这些进程、线程或者协程通常会在多个请求中重复使用,因此,全局变量的设置需要尤为注意,它们在不同的请求中应该是被隔离的。
Flask 提供的“全局”变量,如 request
、session
以及供开发人员使用的 g
,在同一个请求的处理过程中是“全局”的,而 Flask(借助底层的 Werkzeug)小心的处理这些“全局”信息,不会侵入其它的请求中,开发人员无需在意使用的是何种并发技术以及并发部件是否被重复使用。
简单来说,当开发人员需要设置在一个请求过程中共享的全局变量时,应该将这些变量作为 g
的属性,Flask 知道应该如何处理它们。
此前将全局这个词打上引号,是因为更确切的说,这些变量是 Werkzeug 所谓的上下文局部变量,Werkzeug 文档中有专门的论述,值得阅读。
teardown_request
回调
之前我们提到,Flask 只专注与获取请求和生成响应,这两个环节中间的业务逻辑,开发人员有完全的自主。当我们进行业务逻辑的开发时,可能会在请求开始申请一些资源,当请求结束时,无论成功或者失败,我们都需要释放这些资源,这时候,我们就需要使用 teardown_request
回调了。
此外,如果不是针对请求上下文,而是针对应用程序上下文结束时进行的清理操作,可以使用 teardown_appcontext
回调。