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 路由系统的另一个设计决策是,Werkzeug 中的路由尝试确保 URL 是唯一的。Werkzeug 会在这方面走得很远,因为它会在路由不明确时自动重定向到规范 URL。

一个模板引擎

Flask 决定使用一个模板引擎:Jinja2。为什么 Flask 没有可插拔的模板引擎接口?您显然可以使用不同的模板引擎,但 Flask 仍然会为您配置 Jinja2。虽然 Jinja2 始终 配置的限制可能会消失,但捆绑一个模板引擎并使用它的决定不会改变。

模板引擎就像编程语言,每个引擎都对事物的工作方式有一定的理解。从表面上看,它们的工作方式都相同:您告诉引擎使用一组变量评估模板,并将返回值作为字符串。

但这几乎是相似之处的终点。例如,Jinja2 具有广泛的过滤器系统、进行模板继承的特定方式、对可重用块(宏)的支持,这些宏可以从模板内部和 Python 代码中使用,支持迭代模板渲染、可配置语法等等。另一方面,像 Genshi 这样的引擎基于 XML 流评估,通过考虑 XPath 的可用性来进行模板继承等等。另一方面,Mako 将模板视为类似于 Python 模块。

当涉及到将模板引擎与应用程序或框架连接起来时,不仅仅是渲染模板。例如,Flask 使用 Jinja2 广泛的自动转义支持。它还提供了从 Jinja2 模板访问宏的方法。

一个不剥夺模板引擎独特功能的模板抽象层本身就是一门科学,对于像 Flask 这样的微框架来说,这是一项过于庞大的任务。

此外,扩展可以很容易地依赖于一个模板语言的存在。您可以轻松地使用自己的模板语言,但扩展仍然可以依赖于 Jinja 本身。

“微型”是什么意思?

“微型”并不意味着您的整个 Web 应用程序必须适合单个 Python 文件(尽管它当然可以),也不意味着 Flask 缺乏功能。“微型框架”中的“微型”意味着 Flask 旨在保持核心简单但可扩展。Flask 不会为您做出许多决定,例如使用哪个数据库。它确实做出的那些决定,例如使用哪个模板引擎,很容易更改。其他一切都取决于您,因此 Flask 可以成为您需要的一切,而不是您不需要的任何东西。

默认情况下,Flask 不包含数据库抽象层、表单验证或任何其他已经存在可以处理该问题的不同库的东西。相反,Flask 支持扩展,以将此类功能添加到您的应用程序,就像它在 Flask 本身中实现一样。许多扩展提供了数据库集成、表单验证、上传处理、各种开放式身份验证技术等等。Flask 可能是“微型”的,但它已准备好在各种需求上投入生产使用。

为什么 Flask 称自己为微框架,但它却依赖于两个库(即 Werkzeug 和 Jinja2)。为什么不应该这样呢?如果我们看看 Web 开发的 Ruby 方面,我们有一个与 WSGI 非常相似的协议。只是在那里它被称为 Rack,但除此之外,它看起来非常像 Ruby 的 WSGI 演绎。但是,几乎所有 Ruby 领域的应用程序都不是直接使用 Rack,而是在一个同名库之上运行。这个 Rack 库在 Python 中有两个等价物:WebOb(以前的 Paste)和 Werkzeug。Paste 仍然存在,但根据我的理解,它有点被弃用,转而支持 WebOb。WebOb 和 Werkzeug 的开发并排开始,并考虑到相似的想法:成为 WSGI 的良好实现,供其他应用程序利用。

Flask 是一个框架,它利用 Werkzeug 已完成的工作来正确地与 WSGI 接口(有时这可能是一项复杂的任务)。由于 Python 包基础设施的最新发展,具有依赖项的包不再是问题,并且几乎没有理由反对拥有依赖于其他库的库。

线程局部变量

Flask 使用线程局部对象(实际上是上下文局部对象,它们也支持 greenlet 上下文)用于请求、会话和一个您可以放置自己东西的额外对象 (g)。为什么会这样,这难道不是一个坏主意吗?

是的,通常使用线程局部变量不是一个好主意。它们给不基于线程概念的服务器带来麻烦,并使大型应用程序更难维护。然而,Flask 并非专为大型应用程序或异步服务器而设计。Flask 旨在快速轻松地编写传统的 Web 应用程序。

Async/await 和 ASGI 支持

Flask 通过在单独的线程上执行协程而不是像异步优先 (ASGI) 框架那样在主线程上使用事件循环,来支持用于视图函数的 async 协程。为了使 Flask 与在 async 引入 Python 之前构建的扩展和代码保持向后兼容性,这是必要的。由于线程开销,这种折衷方案与 ASGI 框架相比引入了性能成本。

由于 Flask 的代码与 WSGI 的联系如此紧密,因此尚不清楚是否有可能使 Flask 类同时支持 ASGI 和 WSGI。Werkzeug 目前正在进行与 ASGI 协同工作的工作,这最终也可能在 Flask 中启用支持。

有关更多讨论,请参阅 使用 async 和 await

Flask 是什么,Flask 不是什么

Flask 永远不会有数据库层。它不会有表单库或任何其他类似的东西。Flask 本身只是桥接到 Werkzeug 以实现正确的 WSGI 应用程序,并桥接到 Jinja2 以处理模板。它还绑定到一些常见的标准库包,例如日志记录。其他一切都取决于扩展。

为什么会这样?因为人们有不同的偏好和要求,如果 Flask 将任何这些强制添加到核心中,它就无法满足这些要求。大多数 Web 应用程序都需要某种模板引擎。然而,并非每个应用程序都需要 SQL 数据库。

随着代码库的增长,您可以自由地做出适合您项目的设计决策。Flask 将继续为 Python 提供的最佳功能提供一个非常简单的粘合层。您可以在 SQLAlchemy 或其他数据库工具中实现高级模式,根据需要引入非关系数据持久性,并利用为 WSGI(Python Web 接口)构建的与框架无关的工具。

Flask 的理念是为所有应用程序构建一个良好的基础。其他一切都取决于您或扩展。