Tornado 实现 decorator 路由

我也实现了一个 Flask decorator 风格的 Tornado 路由,实现的方式极大地参考了 Flask 的过程(add_url_rule 和 Blueprint)。
Tornado 在新建 Application 的时候, 需要传入一个 handlers 参数, 原本这个 handlers 需要自己手动构建: 收集各个 RequestHandler,给他们分配路径,组成一个 handlers tuple。 这样维护起来非常麻烦。
我实现的功能是:

  1. 通过 decorator 给每个 RequestHander 即时分配 url pattern;
  2. 支持根据 API 版本和 Resource 类型自动给 url pattern 添加前缀;
  3. 可以通过 RequestHander 的类名反向查出 url。
    功能一点都不复杂, 实现起来也简单, 不到 100 行代码。下面是我的实现过程。

    一、理念

    后端提供的 API 应该组织起来, 比如 api_v1, api_v2, api_test, api_internal。这里可以抽象出一个 API Class。
    每一套 API 其实是一系列资源的集合, 比如 user, video, post。 这里又可以抽象出一个 Resource Class, 通过 API Class 进行管理。
    每一个资源其实就对应到 Tornado 的 RequestHandler 了。
    所以伪代码应该是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apiv1 = API(name='api_v1', prefix='/api/v1')  # 实例化一个 API Class
    user = apiv1.create_resource('user', prefix='/user') # 通过 API 对象管理 resource
    @user.route('/profile/') # 通过 resource 对象管理 handler, 完整路径是 /api/v1/user/profile
    class UserProfile(BaseHandler):
    @authenticated
    def get(self):
    return self.write('user profile')
    apiv1.register_resources() # 注册所有 resource 到 API 对象下
    app = Application(
    handlers=apiv1.handlers # 所有 handler 可以通过 api.handlers 获取
    )

    二、API Class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    class API:
    def __init__(self, name, prefix):
    self.name = name
    if not prefix.endswith('/'):
    prefix += '/'
    self.prefix = prefix
    self.handlers = []
    self.resources = []

    def route(self, path, kwargs=None, name=None):
    if path.startswith('/'):
    path = path[1:]
    path = '{0}{1}'.format(self.prefix, path)
    def handler_wrapper(handler):
    self.handlers.append((path, handler, kwargs, name))
    def handler_initializer(*args, **kwargs):
    return handler(*args, **kwargs)
    return handler_initializer
    return handler_wrapper

    def route_handler(self, path, handler, kwargs=None, name=None):
    if self.prefix:
    if path.startswith('/'):
    path = path[1:]
    path = '{0}{1}'.format(self.prefix, path)

    self.handlers.append((path, handler, kwargs, name))

    def create_resource(self, name, prefix):
    name = '{0}.{1}'.format(self.name, name)
    resource = _Resource(name, prefix)
    self.resources.append(resource)
    return resource

    def register_source(self, source):
    records = source.route_records
    for record in records:
    record(self)

    def register_resources(self):
    for resource in self.resources:
    self.register_source(resource)

三、Resource Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class _Resource:
def __init__(self, name, prefix):
self.name = name
if not prefix.endswith('/'):
prefix += '/'
self.prefix = prefix
self.route_records = []

def route(self, path, kwargs=None, name=None):
prefix = self.prefix
if path.startswith('/'):
path = path[1:]
path = '{0}{1}'.format(prefix, path)

def handler_wrapper(handler):
if name is None:
handler_name = '{0}.{1}'.format(self.name, handler.__name__)
else:
handler_name = name
self.route_records.append(
lambda x: x.route_handler(path, handler, kwargs=kwargs, name=handler_name))
return handler
return handler_wrapper