最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第十五章,我将使用适用于大型应用的风格重构本应用。
供您参考,以下是本系列文章的列表。
注意1:如果您正在寻找本教程的旧版本,请在此处。
注意2:如果您想在此博客上支持我的工作,或者只是没有耐心等待每周的文章,我将提供完整的本教程版本,打包成电子书或视频集。欲了解更多信息,请访问courses.miguelgrinberg.com。
Microblog已经是一个初具规模的应用了,所以我认为这是讨论Flask应用如何在持续增长中不会变得混乱和难以管理的好时机。 Flask是一个框架,旨在让你选择以任何方式来组织项目,基于该理念,在应用日益庞大或者技能水平变化的时候,才有可能更改和调整其结构。
在本章中,我将讨论适用于大型应用的一些模式,并且为了演示他们,我将对Microblog项目的结构进行一些更改,目标是使代码更易于维护和组织。 当然,在真正的Flask精神中,我鼓励你在尝试决定组织自己的项目的方式时仅仅将这些更改作为参考。
当前状态下的应用程序有两个基本问题。如果查看应用程序的结构,您会注意到可以识别一些不同的子系统,但是支持它们的代码都是混杂在一起的,没有明确的界限。让我们回顾一下这些子系统是什么:
考虑到我已经确定的这三个子系统以及它们的结构,您可能会注意到一个模式。到目前为止,我一直遵循的组织逻辑是不同的应用功能归属到其专属的模块。这些模块中,有一个视图模块,另一个用于Web表单,一个用于错误,一个用于电子邮件,一个用于HTML模板的目录等等。尽管对于小型项目而言,这是一个有意义的结构,但是一旦项目开始发展,它往往会使其中一些模块真正变得庞大而混乱。
更清晰地理解这个问题的一种方法,是思考如何通过尽可能多地重用该项目来启动第二个项目。例如,用户身份验证部分应该可以在其他应用中很好地运行,但是如果您想按原样使用该代码,则必须进入几个模块并将相关部分复制/粘贴到新项目的新文件中。看到这有多么不便?如果该项目将所有与身份验证相关的文件与应用的其余部分分开,那会不会更好吗?Flask的蓝图功能有助于实现更实用的组织,从而使重用代码更加容易。
还有第二个问题并不那么明显。Flask应用实例在app / __ init__.py中创建为全局变量,然后由许多应用模块导入。尽管这本身不是问题,但将应用实例作为全局变量会使某些情况(尤其是与测试相关的情况)复杂化。假设您要在不同的配置下测试此应用。因为应用被定义为全局变量,实际上没有办法使用不同配置变量来实例化的两个应用实例。另一个不理想的情况是,所有测试都使用相同的应用,因此测试可能会对应用进行更改,从而影响稍后运行的其它测试。理想情况下,您希望所有测试都在原始应用实例上运行。
可以在tests.py模块中看到我正在使用的应用实例化之后修改配置的技巧,以指示测试时使用内存数据库而不是默认的SQLite数据库。我真的没有其他方法可以更改已配置的数据库,因为到测试启动时,已创建并配置了应用。对于这种特殊情况,在将配置应用于应用之后更改配置似乎可以正常工作,但是在其他情况下则可能无法正常工作,并且在任何情况下,这都是一种不好的做法,因为这么做可能会导致提示晦涩并且难以找到BUG。
更好的解决方案是不为应用程序使用全局变量,而是使用应用工厂函数在运行时创建它。该函数将接受配置对象作为参数,并返回配置完成的Flask应用实例。 如果我能够通过应用工厂函数来修改应用,那么编写需要特殊配置的测试将变得很容易,因为每个测试都可以创建自己的应用。
在本章中,我我将通过为上面提到的三个子系统重构应用来介绍blueprints。向您显示更改的详细列表将是不切实际的,因为作为应用一部分的每个文件都有少许变化,因此,我将讨论执行重构的步骤,然后您可以下载所做的更改并下载应序。
在Flask中,蓝图是代表应用程序子集的逻辑结构。蓝图可以包含诸如路由,视图函数,表单,模板和静态文件之类的元素。如果在单独的Python包中编写蓝图,那么你将拥有一个封装了应用特定功能的组件。
蓝图的内容最初处于休眠状态。要关联这些元素,需要将蓝图注册到应用中。在注册期间,所有添加到蓝图中的元素都将传递到应用。因此,您可以将蓝图视为应用功能的临时存储,以帮助组织代码。
我创建的第一个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模板子目录。 之后,我将蓝图创建添加到app/errors/__init__.py
模块,并在创建应用实例之后,将蓝图注册到app/__init__.py
。
我必须提一下,Flask蓝图可以为自己的模板和静态文件配置单独的目录。我决定将模板移到应用模板目录的子目录中,以便所有模板都位于单个层次结构中,但是如果您希望将属于蓝图的模板包含在blueprint中,则这也是受支持。例如,如果向Blueprint()
构造函数添加template_folder='templates'
参数,则可以将蓝图的模板存储在app / errors / templates中。
蓝图的创建与应用程序的创建非常相似。这是在蓝图包的___init__.py模块中完成的:
app / errors / __ init__.py:错误蓝图。
from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers
Blueprint
类获取blueprint的名称,基础模块的名称(通常在Flask应用实例中设置为__name__
)以及一些可选参数(在这种情况下我不需要这些参数)。 Blueprint对象创建后,我导入了handlers.py模块,以便其中的错误处理程序在蓝图中注册。 该导入位于底部以避免循环依赖。
在handlers.py模块中,我没有使用@app.errorhandler
装饰器将错误处理程序附加到应用,而是使用了蓝图的@bp.app_errorhandler
装饰器。当两个装饰器最终获得相同的结果时,但这样做的目的是试图使蓝图独立于应用,从而使其更具可移植性。我还需要修改两个错误模板的路径,以说明将它们移到的新errors子目录。
完成错误处理程序重构的最后一步是向应用注册蓝图:
app / __ init__.py:向应用注册错误蓝图。
app = Flask(__name__)
# ...
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
# ...
from app import routes, models # <-- remove errors from this import!
要注册一个蓝图,请使用Flask应用实例的register_blueprint()
方法。注册蓝图后,所有视图函数,模板,静态文件,错误处理程序等都将连接到应用。我将蓝图的导入放在app.register_blueprint()
的正上方,以避免循环依赖。
将应用的身份验证功能重构为蓝图的过程与错误处理程序的过程非常相似。 以下是重构为blueprint的目录层次结构:
app/
auth/ <-- blueprint package
__init__.py <-- blueprint creation
email.py <-- authentication emails
forms.py <-- authentication forms
routes.py <-- authentication routes
templates/
auth/ <-- blueprint templates
login.html
register.html
reset_password_request.html
reset_password.html
__init__.py <-- blueprint registration
要创建此蓝图,我必须将所有认证相关的功能移到为blueprint创建的新模块中。其中包括一些视图函数,Web表单和支持功能,例如通过电子邮件发送密码重置令牌的功能。我也将模板移到一个子目录中,以将它们与应用的其余部分分开,就像我对错误页面所做的那样。
在蓝图中定义路由时,使用@bp.route
装饰器来代替@app.route
装饰器。构建URL中使用的url_for()
语法也有必要的更改。对于直接附加到应用程序的常规视图函数,url_for()
的第一个参数是视图函数名称。在蓝图中定义路由时,此参数必须包含蓝图名称和视图函数名称,并用句点分隔。因此,我不得不用诸如url_for('auth.login')
的代码替换所有出现的url_for('login')
代码,对于其余的视图函数也是如此。
为了向应用注册auth
蓝图,我使用了一种略有不同的格式:
app / __ init__.py:注册用户认证蓝图到应用。
# ...
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
# ...
在这种情况下,register_blueprint()
调用有一个额外的参数url_prefix
。这完全是可选的,但是Flask提供了给blueprint的路由添加URL前缀的选项,因此blueprint中定义的任何路由都会在其完整URL中获取此前缀。在许多情况下,这作为一种“命名空间”很有用,它可以使蓝图中的所有路由与应用或其他蓝图中的其他路由保持分开。对于身份验证,我以为所有路由都以/ auth开头为好,所以我添加了前缀。因此,现在登录URL将为http://localhost:5000/auth/login 。因为我用url_for()来
生成URL,所以所有URL都会自动合并前缀。
第三个蓝图包含核心应用逻辑。重构此蓝图与前两个蓝图的过程一样。我给这个蓝图命名为main
,所以所有引用视图函数的url_for()
调用都必须有一个main.
前缀。鉴于这是应用的核心功能,我决定将模板放在原来的位置。 这不会有什么问题,因为我已将其他两个蓝图中的模板移动到子目录中了。
正如我在本章引言中提到的那样,将应用作为全局变量会带来一些复杂性,主要是某些测试方案的局限性。在介绍蓝图之前,应用必须是一个全局变量,因为所有视图函数和错误处理程序都需要使用来自app
的装饰器来修饰,比如@app.route
。但是现在所有的路由和错误处理程序都移到了蓝图上,那么保持应用全局性的理由就不够充分了。
因此,我要做的是添加一个名为create_app()
的函数来构造一个Flask应用实例,并消除全局变量。转换并非易事,我不得不理清一些复杂问题,但让我们首先来看一下应用工厂函数:
app / __ init__.py:应用工厂函数。
# ...
db = SQLAlchemy()
migrate = Migrate()
login = LoginManager()
login.login_view = 'auth.login'
login.login_message = _l('Please log in to access this page.')
mail = Mail()
bootstrap = Bootstrap()
moment = Moment()
babel = Babel()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
mail.init_app(app)
bootstrap.init_app(app)
moment.init_app(app)
babel.init_app(app)
# ... no changes to blueprint registration
if not app.debug and not app.testing:
# ... no changes to logging setup
return app
您已经看到,大多数Flask插件都是通过创建插件实例并将应用作为参数传递来初始化的。当应用不作为全局变量存在时,有一种替代模式,插件分成两个阶段进行初始化。 插件实例首先像前面一样在全局范围内创建,但没有参数传递给它。 这会创建一个未附加到应用的插件实例。 当应用实例在工厂函数中创建时,必须在插件实例上调用init_app()
方法,以将其绑定到现在已知的应用。
初始化期间执行的其他任务保持不变,但会移至工厂函数,而不是处于全局范围内。这包括蓝图的注册和日志记录配置。请注意,我在条件语句中添加了一个not app.testing
子句,该子句用来决定是否启用电子邮件和文件日志记录,以便在单元测试期间跳过所有这些日志记录。 由于在配置中TESTING
变量在单元测试时会被设置为True
,因此app.testing
标志在运行单元测试时将变为True
。
那么谁调用了应用工厂函数呢?最明显使用此函数的地方是处于顶级目录的microblog.py脚本,它是唯一会将应用设置为全局变量的模块。 另一个调用该工厂函数的地方是tests.py,我将在下一节中更详细地讨论单元测试。
正如我上面提到的,大多数对app
的引用都是随着blueprint的引入而消失的,但是我仍然需要解决代码中的一些问题。 例如,app/models.py、app/translate.py和app/main/routes.py模块都引用了app.config
。 幸运的是,Flask开发人员试图使视图函数很容易地访问应用实例,而不必像我一直在做的那样导入它。 Flask提供的current_app
变量是一个特殊的“上下文”变量,Flask在分派请求之前使用应用初始化该变量。 你之前已经看到另一个上下文变量,即存储当前语言环境的g
变量。 这两个变量,以及Flask-Login的current_user
和其他一些你还没有看到的东西,是“魔法”变量,因为它们像全局变量一样工作,但只能在处理请求期间且在处理它的线程中访问。
用Flask的current_app
变量替换app
就不需要将应用实例作为全局变量导入。 通过简单的搜索和替换,我可以毫无困难地用current_app.config
替换对app.config
的所有引用。
app/email.py模块提出了一个更大的挑战,所以我必须使用一个小技巧:
app / email.py:将应用实例传递到另一个线程。
from flask import current_app
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
Thread(target=send_async_email,
args=(current_app._get_current_object(), msg)).start()
在send_email()
函数中,应用实例作为参数传递给后台线程,该线程随后将在不阻止主应用程序的情况下发送电子邮件。在作为后台线程运行的send_async_email()
函数中直接使用current_app
将不会奏效,因为current_app
是一个与处理客户端请求的线程绑定的上下文感知变量。在另一个线程中,current_app
没有赋值。直接将current_app
作为参数传递给线程对象也不会有效,因为current_app
实际上是一个代理对象,它被动态地映射到应用实例。因此,传递代理对象与直接在线程中使用current_app
相同。我需要做的是访问存储在代理对象中的实际应用程序实例,并将其作为app
参数传递。 current_app._get_current_object()
表达式从代理对象中提取实际的应用实例,所以它就是我作为参数传递给线程的。
另一个棘手的模块是app / cli.py,它实现了一些用于管理语言翻译的快捷命令。在这种情况下,current_app
变量不起作用,因为这些命令是在启动时注册的,而不是在处理请求期间(这是唯一可以使用current_app
的时间段)注册的。 为了在这个模块中删除对app
的引用,我使用了另一个技巧,将这些自定义命令移动到一个将app
实例作为参数的register()
函数中:
app / cli.py:注册自定义应用命令。
import os
import click
def register(app):
@app.cli.group()
def translate():
"""Translation and localization commands."""
pass
@translate.command()
@click.argument('lang')
def init(lang):
"""Initialize a new language."""
# ...
@translate.command()
def update():
"""Update all languages."""
# ...
@translate.command()
def compile():
"""Compile all languages."""
# ...
然后我从microblog.py调用了register()
函数。以下是完成重构后的microblog.py:
microblog.py:重构了主应用模块。
from app import create_app, db, cli
from app.models import User, Post
app = create_app()
cli.register(app)
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post' :Post}
正如我在本章开始所暗示的那样,到目前为止,我所做的许多工作都是以改善单元测试工作流程为目标的。在运行单元测试时,要确保应用的配置方式不会污染开发资源(如数据库)。
tests.py的当前版本采用了应用实例化之后修改配置的技巧,这是一种危险的做法,因为并不是所有类型的更改都会在修改之后才生效。 我想要的是有机会在添加到应用之前指定我想要的测试配置项。
create_app()
函数现在接受一个配置类作为参数。 默认情况下,使用在config.py中定义的Config
类,但现在我可以通过将新类传递给工厂函数来创建使用不同配置的应用实例。 下面是一个适用于我的单元测试的示例配置类:
tests.py:测试配置。
from config import Config
class TestConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
我在这里做的是创建应用的Config
类的子类,并覆盖SQLAlchemy配置以使用内存SQLite数据库。 我还添加了一个TESTING
属性,并设置为True
,我目前不需要该属性,但如果应用需要确定它是否在单元测试下运行,它就派上用场了。
你一定还记得,我的单元测试依赖于setUp()
和tearDown()
方法,它们由单元测试框架自动调用,以创建和销毁每次测试运行的环境。 我现在可以使用这两种方法为每个测试创建和销毁一个测试专用的应用:
tests.py:为每个测试创建一个应用。
class UserModelCase(unittest.TestCase):
def setUp(self):
self.app = create_app(TestConfig)
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
新的应用将存储在self.app
中,但光是创建一个应用不足以使所有的工作都成功。 思考创建数据库表的db.create_all()
语句。 db
实例需要注册到应用实例,因为它需要从app.config
获取数据库URI,但是当你使用应用工厂时,应用就不止一个了。 那么db
如何关联到我刚刚创建的self.app
实例呢?
答案在应用上下文中。 还记得current_app
变量吗?当不存在全局应用实例导入时,该变量以代理的形式来引用应用实例。 这个变量在当前线程中查找活跃的应用上下文,如果找到了,它会从中获取应用实例。 如果没有上下文,那么就没有办法知道哪个应用实例处于活跃状态,所以current_app
就会引发一个异常。 下面你可以看到它是如何在Python控制台中工作的。 这需要通过运行python
启动,因为flask shell
命令会自动激活应用上下文以方便使用。
>>> from flask import current_app
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
Traceback (most recent call last):
...
RuntimeError: Working outside of application context.
>>> from app import create_app
>>> app = create_app()
>>> app.app_context().push()
>>> current_app.config['SQLALCHEMY_DATABASE_URI']
'sqlite:home/miguel/microblog/app.db'
这就是秘密所在!调用视图功能之前,Flask推送一个应用上下文,它会使current_app
和g
生效。 当请求完成时,上下文将与这些变量一起被删除。 为了使db.create_all()
调用在单元测试setUp()
方法中工作,我为刚刚创建的应用实例推送了一个应用上下文,这样db.create_all()
可以使用 current_app.config
知道数据库在哪里。 然后在tearDown()
方法中,我弹出上下文以将所有内容重置为干净状态。
您还应该知道,应用上下文是Flask使用的两个上下文之一。还有一个请求上下文,它更加具体,因为它适用于请求。当在处理请求之前激活请求上下文时,Flask的request
、session
以及Flask-Login的current_user
变量才会变成可用状态。
正如您在构建此应用时所看到的那样,在启动服务器之前,有许多配置选项取决于在环境中设置的变量。这包括您的密钥,电子邮件服务器信息,数据库URL和Microsoft Translator API密钥。您可能会同意我的看法,这很不方便,因为每次您打开新的终端会话时,都需要再次设置这些变量。
应用依赖大量环境变量的常见处理模式是将这些变量存储在应用根目录中的 .env 文件中。 应用在启动时会从此文件中导入变量,这样就不需要你手动设置这些变量了。
有一个支持.env文件的Python包,称为python-dotenv
。因此,让我们安装该软件包:
(venv) $ pip install python-dotenv
由于config.py模块是我读取所有环境变量的地方,因此我将在创建类之前导入.env文件Config
,以便在构造类时设置变量:
config.py:导入带有环境变量的.env文件。
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config(object):
# ...
现在你可以创建一个 .env 文件并在其中写入应用所需的所有环境变量了。不要将 .env 文件加入到源代码版本控制中,这非常重要。否则,一旦你的密码和其他重要信息上传到远程代码库中后,你就会后悔莫及。
.ENV文件可用于所有的配置变量,但它不能用于Flask命令行的FLASK_APP
和FLASK_DEBUG
环境变量,因为它们在应用启动的早期(应用实例和配置对象存在之前)就被使用了。
以下示例显示了 .env 文件,该文件定义了一个安全密钥,将电子邮件配置为在本地运行的邮件服务器的25端口上,并且不进行身份验证,设置Microsoft Translator API key,使用数据库配置的默认值:
SECRET_KEY=a-really-long-and-unique-key-that-nobody-knows
MAIL_SERVER=localhost
MAIL_PORT=25
MS_TRANSLATOR_KEY=<your-translator-key-here>
此时我已经在Python虚拟环境中安装了一定数量的软件包。 如果你需要在另一台机器上重新生成你的环境,将无法记住你必须安装哪些软件包,所以一般公认的做法是在项目的根目录中写一个requirements.txt文件,列出所有依赖的包及其版本。 生成这个列表实际上很简单:
(venv) $ pip freeze > requirements.txt
pip freeze
命令将以对requirements.txt文件正确的格式转储虚拟环境中安装的所有软件包。现在,如果您需要在另一台计算机上创建相同的虚拟环境,而不是一个一个地无需逐个安装软件包,则可以运行一条命令:
(venv) $ pip install -r requirements.txt
原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xv-a-better-application-structure