应用工厂模式
正如我在本章的介绍中所提到的,将应用设置为全局变量会引入一些复杂性,主要是以某些测试场景的局限性为形式。 在我介绍 blueprint 之前,应用必须是一个全局变量,因为所有的视图函数和错误处理程序都需要使用来自 app
的装饰器来修饰,比如 @app.route
。 但是现在所有的路由和错误处理程序都被转移到了 blueprint 中,因此保持应用全局性的理由就不够充分了。
所以我要做的是添加一个名为 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()
方法,以将其绑定到现在已知的应用。
在初始化期间执行的其他任务保持不变,但会被移到工厂函数而不是在全局范围内。 这包括 blueprint 和日志配置的注册。 请注意,我在条件中添加了一个 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 app 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}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论