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 代码内复用块(宏)、所有操作都使用 Unicode 、支持迭代模板渲染以及可配置语法等等。而比如 Genshi 基于 XML 流 赋值,其模板继承基于 XPath 的能力。再如 Mako 使用类似 Python 模块的方式 来处理模板。

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

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

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

我依赖所以我微

为什么 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 不仅为大型应用或异步服务器设计。 Flask 想要使得编写一个传统 web 应用的过程快速而简单。

一些关于基于 Flask 大型应用的灵感,见文档的 聚沙成塔 一节。

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

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

一些关于基于 Flask 大型应用的灵感,见文档的 大型应用 一节。

Flask 是什么,不是什么

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

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

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