返回介绍

应用工厂模式

发布于 2025-01-02 21:53:56 字数 4586 浏览 0 评论 0 收藏 0

正如我在本章的介绍中所提到的,将应用设置为全局变量会引入一些复杂性,主要是以某些测试场景的局限性为形式。 在我介绍 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文