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 为了保持与在 Python 中引入 async
之前构建的扩展和代码向后兼容而必需的。由于线程的开销,这种折衷方案与 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 的理念是为所有应用程序构建良好的基础。其他所有内容都由您或扩展决定。