用面向对象方法组织 Flask 应用程序(二)——组织 Blueprint

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

上一篇文章 我说过示例程序中的 Blueprint 存在设计问题,即 Blueprint 和视图函数存在着循环引用。

我不知道 Flask 的作者为什么起了 Blueprint 这么一个不大好理解的名字。本质上来说,Flask 的 Blueprint 应该类似于 ASP.NET MVC 中的 area,目的是将大的程序划分为几个相对独立的分区来进行管理。然而 名称 area 明显容易理解得多。此外,Blueprint(以及 Flask App)通过装饰器的形式定义路由。装饰器从形式上来讲和 C# Attribute 或者 Java Annonations 非常类似,但内部实现机制大相径庭。C# Attribute 和 Java annonations 是静态的,不需要 app 引用,而 Flask App/Blueprint 则需要先定义 app,然而 app 又无法自动加载定义在其他模块中的视图函数。这样的结果就是,如果 Blueprint/视图函数定义分别定义,则循环引用就无法避免。

我在其他地方看到的另外一种做法是,将 Blueprint/视图函数定义在同一文件中,定义 Blueprint 之后就可以直接引用,不存在问题。这样也是一种可行的方案,而且对小的程序来说也很清晰。但我不太喜欢把分区和视图函数定义在一起的设计,因为从概念上来讲,它们属于两个不同的层次。

我尝试的第一种做法是,将 Blueprint 也定义成类,同时放弃装饰器的形式,直接用 add_url_rule 定义路由:

from flask import Blueprint

class MyBlueprint(Blueprint):
def __init__(self):
super(MyBlueprint, self).__init__('my', __name__)
self.register_routes()

def register_routes(self):
from . import views
self.add_url_rule('/', 'index', views.index)
...

这样 views.py 中不需要再定义装饰器,避免了循环引用。当然这样做的代价就是注册视图函数的代码要写得多一些。

后来我又想:是否可能类似于 C# 或 Java 那样,将路由定义为静态的,从而无需引用 blueprint 呢?当然,静态也就意味着 Blueprint 无法自动发现关联的视图函数,必须自己写一些代码去做这件事情。

基于上述想法,我仿照 Blueprint.route ,定义了一个静态 route 和相关的搜索方法:

def route(rule, **kwargs):
def decorator(f):
    endpoint = kwargs['name'] if 'name' in kwargs else f.__name__
    f.__rulemeta__ = dict(rule=rule, endpoint=endpoint)
    if 'methods' in kwargs:
        f.__rulemeta__['methods'] = kwargs['methods']
    return f
return decorator


def collect_url_rules(blueprint, mod):
    """search mod for all view functions who is registered
    using @route, and register it into blueprint"""
    import types
    for k in dir(mod):
        fn = getattr(mod, k)
        if isinstance(fn, types.FunctionType) and hasattr(fn, '__rulemeta__'):
            meta = fn.__rulemeta__
            rule = meta['rule']
            endpoint = meta['endpoint']
            methods = meta.get('methods', ['GET'])
            blueprint.add_url_rule(rule,
                                   endpoint=endpoint,
                                   view_func=fn,
                                   methods=methods)

这样视图函数形式可以基本不变,只要去掉装饰器前缀:

from utils.flask_util import route

@route('/')
def index():
return render_template('my/index.html')

...

同时 Blueprint 定义修改为

from flask import Blueprint
from utils.flask_util import collect_url_rules

class MyBlueprint(Blueprint):
def __init__(self):
super(MyBlueprint, self).__init__('my', __name__)
self.register_routes()

def register_routes(self):
from . import views
collect_url_rules(self, views)