第十五章:优化应用结构
这是Flask Mega-Tutorial系列的 第十五部分,我将使用适用于大型应用的风格重构本应用。
Microblog已经是一个初具规模的应用了,所以我认为这是讨论Flask应用如何在持续增长中不会变得混乱和难以管理的好时机。 Flask是一个框架,旨在让你选择以任何方式来组织项目,基于该理念,在应用日益庞大或者技能水平变化的时候,才有可能更改和调整其结构。
在本章中,我将讨论适用于大型应用的一些模式,并且为了演示他们,我将对Microblog项目的结构进行一些更改,目标是使代码更易于维护和组织。 当然,在真正的Flask精神中,我鼓励你在尝试决定组织自己的项目的方式时仅仅将这些更改作为参考。
目前状态下的应用有两个基本问题。 如果你观察应用的组织方式,你会注意到有几个不同的子系统可以被识别,但支持它们的代码都混合在了一起,没有任何明确的界限。 我们来回顾一下这些子系统是什么:
- 用户认证子系统,包括app/routes.py中的一些视图函数,app/forms.py中的一些表单,app/templates中的一些模板以及app/email.py中的电子邮件支持。
- 错误子系统,它在app/errors.py中定义了错误处理程序并在app/templates中定义了模板。
- 核心应用功能,包括显示和撰写用户动态,用户个人主页和关注以及用户动态的实时翻译,这些功能遍布大多数应用模块和模板。
思考这三个子系统以及它们组织的方式,你可能会注意到这样一个模式。 到目前为止,我一直遵循的组织逻辑是不同的应用功能归属到其专属的模块。 这些模块之中,一个用于视图函数,一个用于Web表单,一个用于错误,一个用于电子邮件,一个目录用于存放HTML模板等等。 虽然这是一个对小项目有意义的组织结构,但是一旦项目开始增长,它往往会使其中的一些模块变得非常大而且杂乱无章。
要想清晰地看到问题的一种方法,是思考如何通过尽可能多地重复使用这一项目来开始第二个项目。 例如,用户身份验证部分应该在其他应用中也能运行良好,但如果你想按原样使用该代码,则必须进入多个模块并将相关部分复制/粘贴到新项目的新文件中。 看到这是多么不方便了吗? 如果这个项目将所有与认证相关的文件从应用的其余部分中分离出来,会不会更好? Flask的blueprints功能有助于实现更实用的组织结构,从而更轻松地重用代码。
还有第二个问题,虽然它不太明显。 Flask应用实例在
app/__init__.py
中被创建为一个全局变量,然后又被很多应用模块导入。 虽然这本身并不是问题,但将应用实例作为全局变量可能会使某些情况复杂化,特别是与测试相关的情景。 想象一下你想要在不同的配置下测试这个应用。 由于应用被定义为全局变量,实际上没有办法使用不同配置变量来实例化的两个应用实例。 另一种糟心的情况是,所有测试都使用相同的应用,因此测试可能会对应用进行更改,就会影响稍后运行的其他测试。 理想情况下,你希望所有测试都在原始应用实例上运行的。你可以在tests.py模块中看到我正在使用的应用实例化之后修改配置的技巧,以指示测试时使用内存数据库而不是默认的SQLite数据库。我真的没有其他办法来更改已配置的数据库,因为在测试开始时已经创建和配置了应用。 对于这种特殊情况,对已配置的应用实例修改配置似乎可以运行,但在其他情况下可能不会,并且在任何情况下,这是一种不推荐的做法,因为这么做可能会导致提示晦涩并且难以找到BUG。
更好的解决方案是不将应用设置为全局变量,而是使用应用工厂函数在运行时创建它。 这将是一个接受配置对象作为参数的函数,并返回一个配置完毕的Flask应用实例。 如果我能够通过应用工厂函数来修改应用,那么编写需要特殊配置的测试会变得很容易,因为每个测试都可以创建它各自的应用。
在Flask中,blueprint是代表应用子集的逻辑结构。 blueprint可以包括路由,视图函数,表单,模板和静态文件等元素。 如果在单独的Python包中编写blueprint,那么你将拥有一个封装了应用特定功能的组件。
Blueprint的内容最初处于休眠状态。 为了关联这些元素,blueprint需要在应用中注册。 在注册过程中,需要将添加到blueprint中的所有元素传递给应用。 因此,你可以将blueprint视为应用功能的临时存储,以帮助组织代码。
我创建的第一个blueprint用于封装对错误处理程序的支持。 该blueprint的结构如下:
app/
errors/ <-- blueprint package
__init__.py <-- blueprint creation
handlers.py <-- error handlers
templates/
errors/ <-- error templates
404.html
500.html
__init__.py <-- blueprint registration
实质上,我所做的是将app/errors.py模块移动到app/errors/handlers.py中,并将两个错误模板移动到app/templates/errors中,以便将它们与其他模板分开。 我还必须在两个错误处理程序中更改
render_template()
调用以使用新的errors模板子目录。 之后,我将blueprint创建添加到app/errors/__init__.py
模块,并在创建应用实例之后,将blueprint注册到app/__init__.py
。我必须提一下,Flask blueprints可以为自己的模板和静态文件配置单独的目录。 我已决定将模板移动到应用模板目录的子目录中,以便所有模板都位于一个层次结构中,但是如果你希望在blueprint中包含属于自己的模板,这也是支持的。 例如,如果向
Blueprint()
构造函数添加template_folder='templates'
参数,则可以将错误blueprint的模板存储在app/errors/templates目录中。创建blueprint与创建应用非常相似。 这是在blueprint的
___init__.py
模块中完成的:app/errors/__init__.py
:错误blueprint。from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers
Blueprint
类获取blueprint的名称,基础模块的名称(通常在Flask应用实例中设置为__name__
)以及一些可选参数(在这种情况下我不需要这些参数)。 Blueprint对象创建后,我导入了handlers.py模块,以便其中的错误处理程序在blueprint中注册。 该导入位于底部以避免循环依赖。在handlers.py模块中,我放弃使用
@app.errorhandler
装饰器将错误处理程序附加到应用程序,而是使用blueprint的@bp.app_errorhandler
装饰器。 尽管两个装饰器最终都达到了相同的结果,但这样做的目的是试图使blueprint独立于应用,使其更具可移植性。我还需要修改两个错误模板的路径,因为它们被移动到了新errors子目录。完成错误处理程序重构的最后一步是向应用注册blueprint:
app/__init__.py
:向应用注册错误blueprint。app = Flask(__name__)