Flask 的设计思路

为什么 Flask 要这样做,而不是那样做?如果你对这点好奇,那么本节可以满 足你的好奇心。当与其他框架直接进行比较时, Flask 的设计思路乍看可能显 得武断并且令人吃惊,下面我们就来看看为什么在设计的时候进行这样决策。

显式的应用对象

一个基于 WSGI 的 Python web 应用必须有一个实现实际的应用的中心调用对 象。在 Flask 中,中心调用对象是一个 Flask 类的实例。 每个 Flask 应用必须创建一个该类的实例,并且把模块的名称传递给该实例。 但是为什么 Flask 不自动把这些事都做好呢?

下面的代码:

from flask import Flask
app = Flask(__name__)

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

如果没有一个显式的应用对象,那么会是这样的:

from hypothetical_flask import route

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

使用对象的主要有三个原因。最重要的一个原因是显式对象可以保证实例的唯 一性。有很多方法可以用单个应用对象来冒充多应用,比如维护一个应用堆栈, 但是这样将会导致一些问题,这里我就不展开了。现在的问题是:一个微框架 何时会需要多应用?最好的回答是当进行单元测试的时候。在进行测试时,创 建一个最小应用用于测试特定的功能,是非常有用的。当这个最小应用的应用 对象被删除时,将会释放其占用的所有资源。

另外当使用显式对象时,你可以继承基类( Flask ), 以便于修改特定的功能。如果不使用显式对象,那么就无从下手了。

第二个原因也很重要,那就是 Flask 需要包的名称。当你创建一个 Flask 实 例时,通常会传递 __name__ 作为包的名称。 Flask 根据包的名称来载入与 模块相关的正确资源。通过 Python 杰出的反射功能,就可以找到模板和静态 文件(参见 open_resource() )。很显然,有其他的框 架不需要任何配置就可以载入与模块相关的模板。但其前提是必须使用当前工 作目录,这是一个不可靠 的实现方式。当前工作目录是进程级的,如果有多个 应用使用同一个进程( web 服务器可能在你不知情的情况下这样做),那么当 前工作目录就不可用了。还有更糟糕的情况:许多 web 服务器把文档根目录作 为当前工作目录,如果你的应用所在的目录不是文档根目录,那么就会出错。

第三个原因是“显式比隐式更好”。这个对象就是你的 WSGI 应用,你不必再 记住其他东西。如果你要实现一个 WSGI 中间件,那么只要封装它就可以了 (还有更好的方式,可以不丢失应用对象的引用,参见: wsgi_app() )。

再者,只有这样设计才能使用工厂函数来创建应用,方便单元测试和类似的工 作(参见: 应用工厂 )。

路由系统

Flask 使用 Werkzeug 路由系统,该系统是自动根据复杂度来为路由排序的。 也就是说你可以以任意顺序来声明路由,路由系统仍然能够正常工作。为什么 要实现这个功能?因为当应用被切分成多个模块时,基于路由的装饰器会以乱 序触发,所以这个功能是必须的。

另一点是 Werkzeug 路由系统必须确保 URL 是唯一的,并且会把模糊路由重定 向到标准的 URL 。

唯一模板引擎

Flask 原生只使用 Jinja2 模板引擎。为什么不设计一个可插拔的模板引擎接 口?当然,你可以在 Flask 中使用其他模板引擎,但是当前 Flask 原生只会 支持 Jinja2 。将来也许 Flask 会使用其他引擎,但是永远只会绑定一个模板 引擎。

模板引擎与编程语言类似,每个引擎都有自己的一套工作方式。表面上它们都 看上去差不多:你把一套变量丢给引擎,然后得到字符串形式的模板。

但是相似之处也仅限于此。例如 Jinja2 有丰富的过滤系统、有一定的模板继 承能力、支持从模板内或者 Python 代码内复用块(宏)、支持迭代模板渲染 以及可配置语法等等。而比如 Genshi 基于 XML 流赋值,其模板继承基于 XPath 的能力。再如 Mako 使用类似 Python 模块的方式来处理模板。

当一个应用或者框架与一个模板引擎结合在一起的时候,事情就不只是渲染模 板这么简单了。例如, Flask 使用了 Jinja2 的强大的自动转义功能。同时 Flask 也为 Jinja2 提供了在模板中操作宏的途径。

一个不失模板引擎独特性的模板抽象层本身就是一门学问,因此这不是一个 Flask 之类的微框架应该考虑的事情。

此外,只使用一个模板语言可以方便扩展。你可以使用你自己的模板语言,但 扩展仍然使用 Jinja 。

“微”的含义

“微”并不代表整个应用只能塞在一个 Python 文件内,当然塞在单一文件内 也没有问题。 “微”也不代表 Flask 功能不强。微框架中的“微”字表示 Flask 的目标是保持核心简单而又可扩展。 Flask 不会替你做出许多决定,比 如选用何种数据库。类似的决定,如使用何种模板引擎,是非常容易改变的。 Flask 可以变成你任何想要的东西,一切恰到好处,由你做主。

缺省情况下, Flask 不包含数据库抽象层、表单验证或者其他已有的库可以处 理的东西。然而, Flask 通过扩展为应用添加这些功能,就如同这些功能是 Flask 原生的一样。大量的扩展用以支持数据库整合、表单验证、上传处理和 各种开放验证等等。 Flask 可能是“微小”的,但它已经为满足您的各种生产 需要做出了充足的准备。

为什么 Flask 依赖两个库( Werkzeug 和 Jinja2 ),但还是自称是微框架? 为什么不可以呢?如果我们看一看 Web 开发的另一大阵营 Ruby ,那么可以发 现一个与 WSGI 十分相似的协议。这个协议被称为 Rack ,除了名称不同外, 基本可以视作 Ruby 版的 WSGI 。但是几乎所有 Ruby 应用都不直接使用 Rack 协议,而是使用一个相同名字的库。在 Python 中,与 Rack 库等价的有 WebOb (前身是 Paste )和 Werkzeug 两个库。 Paste 任然可用,但是个人 认为正逐步被 WebOb 取代。WebOb 和 Werkzeug 的开发初衷都是:做一个 WSGI 协议的出色实现,让其他应用受益。

正因为 Werkzeug 出色地实现了 WSGI 协议(有时候这是一个复杂的任务), 使得依赖于 Werkzeug 的 Flask 受益良多。同时要感谢 Python 包管理的近期 开发,包依赖问题已经解决,几乎没有理由不使用包依赖的方式。

线程本地对象

Flask 使用线程本地对象(实际是情境本地对象,它们也支持 greenlet 情境) 来支持请求、会话和一个可以放置你自己的东西的额外对象( g )。为什么要这样做?这不是一个坏主意吗?

是的,通常情况下使用线程本地对象不是一个明智的选择,这会在不是基于线 程理念的服务器上造成麻烦,并且加大大型应用的维护难度。但是 Flask 不仅 是为大型应用或异步服务器设计的,Flask 还想简化和加速传统 web 应用的开 发。

Async/await 和 ASGI 支持

Flask 视图函数支持 async 协程,这是通过在单独的线程中执行协程实现 的,而不是像异步优先( ASGI )的框架那样,通过在主线程上使用一个事件 循环来实现的。这样做是为了向后兼容那些在 async 引入 Python 之前所 编写的程序和扩展。与 ASGI 框架相比,这种妥协方式会使用更多线程,带来 更多的性能开销。

因为无法确定 Flask 的代码与 WSGI 的关系有多紧密,所以尚不清楚 Flask 类是否能同时支持 ASGI 和 WSGI 。目前正在进行的工作是使 Werkzeug 支持 ASGI ,随后也会跟进 Flask 的支持工作。

更多讨论,参见 使用 async 和 await

Flask 是什么,不是什么

Flask 永远不会包含数据库层,也不会有表单库或是这个方面的其它东西。 Flask 本身只是 Werkezug 和 Jinja2 的之间的桥梁,前者实现一个合适的 WSGI 应用,后者处理模板。 当然, Flask 也绑定了一些通用的标准库包,比 如 logging 。 除此之外其它所有一切都交给扩展来实现。

为什么呢?因为人们有不同的偏好和需求, Flask 不可能把所有的需求都囊括 在核心里。大多数 web 应用会需要一个模板引擎。然而不是每个应用都需要一 个 SQL 数据库的。

随着你的代码库的增长,你可以自由地做出适合你的项目的设计决定。 Flask 将一如既往提供一个非常简单的胶合层,做到 Python 所能提供的最好的东西。 你可以在以下方面实现高级模式: SQLAlchemy 或其他数据库工具,引入非关 系型数据持久性并从为 Python 网络接口而构建的 WSGI 框架无关工具受益。

Flask 的理念是为所有应用建立一个良好的基础,其余的一切都取决于你自己 或者扩展。