42

简单解决大型 Flask 蓝图的路由划分 - SHUHARI 的博客

 4 years ago
source link: https://shuhari.dev/blog/2019/10/flask-blueprint-devided?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

简单解决大型 Flask 蓝图的路由划分

版权声明:所有博客文章除特殊声明外均为原创,允许转载,但要求注明出处。

Flask 框架提供了蓝图(Blueprint)的概念,作为划分大型网站的主要工具。但对于地址较为复杂的网站,仅靠 Blueprint 仍然是不够的。以个人博客为例,如果我们把它规划为个人网站的一个子分区,那么很自然地会设置一个 url_prefix='/blog' 的蓝图。但 blog 下面可能不只是一个简单的平面结构,还会有更多的层次,类似这样:

  • /blog/post/
  • /blog/post//edit
  • /blog/post//comments
  • /blog/category
  • /blog/category/

当然,我们仍然可以逐项在路由中添加必要的前缀,但是这样显然违反了 DIY 原则。有没有办法将 Blueprint 再进行细分呢?

遗憾的是,蓝图已经是最小的管理单位,Flask 官方对此并未提供什么解决办法。好在 Flask 是一个比较灵活的框架,俗话说自己动手丰衣足食,但在动手之前,我们不妨先看看其他框架是如何解决这个问题的。

Django 是经常拿来和 Flask 作比较的同类框架。不同于 Flask 的装饰器风格,Django 的设计倾向于在同一个地方(通常是每个 app 中的 urls.py)统一声明所有的路由映射。Django 也支持用 include 的方式声明嵌套形式的地址,大致实现如下:

from django.urls import include, path

nested = [
   path('/', nest_views.index, name='index'),
   ...
]   

urlpatterns = [
    path('/', home_views.index, name='index'),
    path('nested/', include(nested, namespace='nested')),
]

Django 的做法可以给我们一些启发。Flask 虽然鼓励我们用装饰器的方式去声明路由,但同时也提供了 add_url_rule 这种更加灵活的方法,给了我们额外的发挥空间。

我们希望实现一个“缩微版”的 Blueprint,简称为 Area(这个名字是从 ASP.NET MVC 里面“偷”来的)。大致要求为:

  • Blueprint 下面可以挂接多个 Area。对于更复杂的 URL,Area 下面可以再次挂接 Area,理论上可以无限扩展;
  • 同一个 Area 下面的所有路由共享相同的 URL 前缀;
  • 注册路由的接口和 Flask App/Blueprint 保持一致。

了解了要求,实现就很简单了。

class BlueprintArea:
    def __init__(self, owner, prefix):
        self.owner = owner
        self.prefix = prefix

    def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
        full_rule = self.prefix + rule
        endpoint = options.pop("endpoint", None)
        self.owner.add_url_rule(full_rule, endpoint, view_func, **options)

    def route(self, rule, **options):
        def decorator(f):
            self.add_url_rule(rule, view_func=f, **options)
            return f
        return decorator

这里,我们直接借鉴了 Blueprintadd_url_rule 的实现,只是加上了自己的前缀。

然后,我们写个简单的 Flask 应用来测试一下。为了证明 Area 是可以嵌套的,我们在 area1 下面再挂一个 area11。为简洁起见,完整的视图函数就不再列出了:

from flask import Flask, Blueprint

app = Flask(__name__)

@app.route('/')
def index():
    return 'index'

bp = Blueprint('bp', __name__)
area1 = BlueprintArea(bp, '/area1')
area11 = BlueprintArea(area1, '/area11')
area2 = BlueprintArea(bp, '/area2')

@bp.route('/')
def bp_index():
    return 'bp'


@area1.route('/')
def area1_index():
    ...

app.register_blueprint(bp, url_prefix='/bp')

这里有两点值得说明:

  1. 在声明所有路由之后再调用 register_blueprint()。这是因为 register_blueprint 会对所有已声明的路由进行注册,在这之后声明的路由将不起作用。如果将 Flask 应用组织为多个模块的话,请注意模块之间的导入关系。
  2. 尽管 Area 可以(理论上)无穷嵌套,但仍然需要保证对应的视图函数不要重名,或者手工指定 endpoint,否则像 url_for 之类的功能可能会出现问题。

最后,我们写个测试来验证上述代码是否正常工作:

def run_tests():
    cases = [
        ('/bp/', 'bp'),
        ('/bp/area1/', 'area1'),
        ('/bp/area1/123', 'area1 single(123)'),
        ('/bp/area1/area11/', 'area11'),
        ('/bp/area2/', 'area2'),
    ]
    with app.test_client() as client:
        for url, expected in cases:
            actual = client.get(url).get_data(as_text=True)
            assert expected == actual, 'url({}) actual response: {}'.format(url, actual)


if __name__ == '__main__':
    app.run()
else:    
    run_tests()

OK,没有问题。

最后介绍一点相关资料。本文提出的并不是什么新鲜的观点,因为在 StackOverflow 曾经有人提过类似的想法:

Nested Blueprints in Flask?

甚至有人到 Flask 官方提出过类似的需求,但是被否决了:

Nestable blueprints

在 StackOverflow 的回答中,Abhishek Gupta 的思路和本文的办法在原理上是相同的,只是他的实现没有考虑递归嵌套的问题。但我个人意见,不赞成将其称为 Nest Blueprint,因为它实现的只是路由嵌套,而并不具备 Blueprint 所包含的其他大多数管理功能。这并不是什么问题:如果让每个 Area 都有类似过滤器、错误处理之类的功能,那就有点过于复杂了,实际上也无此必要,在 Blueprint 的层次上处理是一个很好的平衡。只是因为路由对大型网站而言数量会非常多,所以才会有额外的管理要求。

如果你的项目需要处理大型 Flask 应用和复杂的路由,可以考虑本文提供的方案。Flask 自身也提供了一些比较高级的管理方法,比如多重应用和扩展 WSGI 等。但对于本文的场景而言它们并不是特别合适。如果你想了解 Flask 本身的扩展方式,请参考 官方文档


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK