用面向对象方法组织 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)