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”前缀的扩展名称。

添加行为

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

一种常见的模式是使用 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 讨论 中提问。