Flask 扩展开发

扩展是为 Flask 应用程序添加功能的附加包。虽然 PyPI 包含许多 Flask 扩展,但你可能找不到符合你需求的扩展。如果是这种情况,你可以创建自己的扩展,并将其发布以供其他人使用。

本指南将展示如何创建 Flask 扩展,以及涉及的一些常见模式和要求。由于扩展可以执行任何操作,因此本指南无法涵盖所有可能性。

了解扩展的最佳方法是查看你使用的其他扩展是如何编写的,并与他人讨论。在我们的 Discord 聊天室GitHub 讨论 中与他人讨论你的设计理念。

最好的扩展共享一些共同模式,以便熟悉使用一个扩展的任何人都不会在使用另一个扩展时感到完全迷失。只有在早期进行协作才能实现这一点。

命名

Flask 扩展通常在其名称中包含 flask 作为前缀或后缀。如果它包装另一个库,它还应该包含库名称。这使得搜索扩展变得容易,并使它们的用途更加清晰。

一个通用的 Python 打包建议是,包索引中的安装名称和 import 语句中使用的名称应该相关。导入名称为小写,单词之间用下划线 (_) 分隔。安装名称为小写或标题大小写,单词之间用破折号 (-) 分隔。如果它包装另一个库,则优先使用与该库名称相同的大小写。

以下是一些示例安装和导入名称

  • Flask-Name 导入为 flask_name

  • flask-name-lower 导入为 flask_name_lower

  • Flask-ComboName 导入为 flask_comboname

  • Name-Flask 导入为 name_flask

扩展类和初始化

所有扩展都需要某个入口点,以便使用应用程序初始化扩展。最常见的模式是创建一个类来表示扩展的配置和行为,并使用 init_app 方法将扩展实例应用到给定的应用程序实例。

class HelloExtension:
    def __init__(self, app=None):
        if app is not None:
            self.init_app(app)

    def init_app(self, app):
        app.before_request(...)

应用程序不存储在扩展中非常重要,不要执行 self.app = app。扩展唯一可以直接访问应用程序的时间是在 init_app 期间,否则它应该使用 current_app

这允许扩展支持应用程序工厂模式,避免在用户代码的其他位置导入扩展实例时出现循环导入问题,并使使用不同配置进行测试变得更容易。

hello = HelloExtension()

def create_app():
    app = Flask(__name__)
    hello.init_app(app)
    return app

在上面,hello 扩展实例独立于应用程序存在。这意味着用户项目中的其他模块可以执行 from project import hello,并在应用程序存在之前在蓝图中使用扩展。

可以使用 Flask.extensions 字典将对扩展的引用存储在应用程序上,或将某些其他状态存储在特定于应用程序上。请注意,这是一个单一名称空间,因此请使用对扩展唯一的名称,例如不带“flask”前缀的扩展名称。

添加行为

扩展可以通过多种方式添加行为。在扩展的 init_app 方法中,可以使用 Flask 对象上可用的任何设置方法。

一种常见模式是使用 before_request() 在每个请求的开始初始化一些数据或连接,然后使用 teardown_request() 在结束时清理它。这可以存储在 g 上,下面将对此进行更详细的讨论。

一种更懒惰的方法是提供一个初始化和缓存数据或连接的方法。例如,ext.get_db 方法可以在首次调用时创建一个数据库连接,以便不使用数据库的视图不会创建连接。

除了在每个视图之前和之后执行某些操作之外,扩展可能还需要添加一些特定视图。在这种情况下,可以定义一个 Blueprint,然后在 init_app 期间调用 register_blueprint() 将蓝图添加到应用程序中。

配置技术

扩展的配置可以有多个级别和来源。你应该考虑扩展的哪些部分属于每一部分。

  • 通过 app.config 值进行每个应用程序实例的配置。这是对于应用程序的每次部署都可能合理更改的配置。一个常见的示例是外部资源的 URL,例如数据库。配置键应以扩展名开头,以便它们不会干扰其他扩展。

  • 通过 __init__ 参数进行每个扩展实例的配置。此配置通常会影响扩展的使用方式,因此在每次部署中更改它没有任何意义。

  • 通过实例属性和装饰器方法进行每个扩展实例的配置。在创建扩展实例后,将值分配给 ext.value 或使用 @ext.register 装饰器来注册函数可能会更符合人体工程学。

  • 通过类属性进行全局配置。更改类属性(如 Ext.connection_class)可以自定义默认行为,而无需创建子类。这可以与每个扩展配置相结合以覆盖默认值。

  • 子类化和重写方法和属性。使扩展本身的 API 成为可以重写的内容,为高级自定义提供了一个非常强大的工具。

Flask 对象本身使用所有这些技术。

根据你的需要和想要支持的内容,由你决定哪种配置适合你的扩展。

在应用程序设置阶段完成后并且服务器开始处理请求后,不应更改配置。配置是全局的,对其进行的任何更改都不保证对其他工作进程可见。

请求期间的数据

在编写 Flask 应用程序时,g 对象用于在请求期间存储信息。例如,教程将与 SQLite 数据库的连接存储为 g.db。扩展也可以谨慎使用此功能。由于 g 是一个单一的全局命名空间,因此扩展必须使用不会与用户数据冲突的唯一名称。例如,使用扩展名称作为前缀或作为命名空间。

# an internal prefix with the extension name
g._hello_user_id = 2

# or an internal prefix as a namespace
from types import SimpleNamespace
g._hello = SimpleNamespace()
g._hello.user_id = 2

g 中的数据持续存在于应用程序上下文中。当请求上下文处于活动状态时,或当 CLI 命令运行时,应用程序上下文处于活动状态。如果你要存储应该关闭的内容,请使用 teardown_appcontext() 以确保在应用程序上下文结束时关闭它。如果它只应在请求期间有效,或在请求之外的 CLI 中不会使用,请使用 teardown_request()

视图和模型

您的扩展视图可能希望与数据库中的特定模型或连接到您的应用程序的其他扩展或数据进行交互。例如,让我们考虑一个 Flask-SimpleBlog 扩展,它与 Flask-SQLAlchemy 一起使用,以提供 Post 模型和视图来编写和阅读帖子。

Post 模型需要子类化 Flask-SQLAlchemy db.Model 对象,但只有在您创建了该扩展的实例后才能使用它,而不是在您的扩展定义其视图时。那么,在模型存在之前定义的视图代码如何访问模型?

一种方法是使用 基于类的视图。在 __init__ 期间,创建模型,然后通过将模型传递给视图类的 as_view() 方法来创建视图。

class PostAPI(MethodView):
    def __init__(self, model):
        self.model = model

    def get(self, id):
        post = self.model.query.get(id)
        return jsonify(post.to_json())

class BlogExtension:
    def __init__(self, db):
        class Post(db.Model):
            id = db.Column(primary_key=True)
            title = db.Column(db.String, nullable=False)

        self.post_model = Post

    def init_app(self, app):
        api_view = PostAPI.as_view(model=self.post_model)

db = SQLAlchemy()
blog = BlogExtension(db)
db.init_app(app)
blog.init_app(app)

另一种技术是使用扩展上的属性,例如上面 self.post_model。在 init_app 中将扩展添加到 app.extensions,然后从视图中访问 current_app.extensions["simple_blog"].post_model

您可能还想提供基类,以便用户可以提供自己的 Post 模型,该模型符合您的扩展预期的 API。因此,他们可以实现 class Post(blog.BasePost),然后将其设置为 blog.post_model

如您所见,这可能会变得有点复杂。不幸的是,这里没有完美的解决方案,只有根据您的需求和您希望提供的自定义程度的不同策略和权衡。幸运的是,大多数扩展并不需要这种资源依赖性。请记住,如果您需要设计方面的帮助,请在我们的 Discord 聊天GitHub 讨论 中提问。