最近在学openstack,发现openstack的好几个组件,都用到了routes.Mapper作为WSGI app的路由控制。nova就在用。nova在用的时候外面又包装了一个类,以后我们再说。底层还是调用了,mapper.connect()去注册路由,调用mapper.routematch去获得路由是否匹配。
下面我们就单纯的时候Mapper去做实验。
入门
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
# 注册一个/hi路由
mapper.connect("/hi")
# 查找/hi
m_result = mapper.routematch("/hi")
print(m_result)
# 查找/hi2
m2_result = mapper.routematch("/hi2")
print(m2_result)
# 查找/hi/boy
m3_result = mapper.routematch("/hi/boy")
print(m3_result)
代码不是很复杂,解释都在上面
下面是我们的运行结果
({}, <routes.route.Route object at 0x000000000380C2E8>)
None
None
routematch返回结果的解释:如果匹配到了,会返回一个二元元组,元组第一个元素为匹配到的字典,第二个元素一个Route对象。我们发现第一个元素是个空字典,这里面会有什么值呢,下面的实验会看到有值。
注册路由时附加key
新写一段代码,如下:
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
# 注册一个/hi路由 并把controller设为say_hi
mapper.connect("/hi", controller="say_hi")
# 查找/hi
m_result = mapper.routematch("/hi")
print(m_result)
这段代码其实,只是在调用connect时多传入了一个key叫做controller,我们看一下运行结果。
({'controller': u'say_hi'}, <routes.route.Route object at 0x00000000034FA2B0>)
我们看到返回的第一个元素已经不是一个空字典了,包含了一个key叫controller正是我们注册路由时,传入的一个键值对。
因为我们光匹配到路由,却不知道交给谁处理,这是没有任何意义的。所以你在nova的注册路由里,发现都传入了controller这个键值对,也出入了action这个键值对。实际传入时controller可能是个对象,可以处理路由。
注册带变量的路由
我们做一个api经常需要一些变量id之类的放到url中,下面这个例子我们就是注册一个带变量的路由
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
# 注册一个/hi路由 需要project_id 并把controller设为say_hi
mapper.connect("/hi/{project_id}", controller="say_hi")
# 查找/hi下project_id=abc
m_result = mapper.routematch("/hi/abc")
print(m_result)
直接看运行结果吧
({'controller': u'say_hi', 'project_id': u'abc'}, <routes.route.Route object at 0x000000000368A2E8>)
我们在match到结果中,可以拿到controller也能拿到url中的变量project_id。
Mapper提供了两种标识变量的写法,除了上面那种还有
mapper.connect("/hi/:project_id", controller="say_hi")
当然两者也可以混着写,但是不建议,因为不美观
mapper.connect("/hi/:project_id/{name}", controller="say_hi")
注册有限制条件的变量的路由
我们有时候,需要限制一些变量的格式,例如project_no只能是数字,project_id只能是数字字母下划线之类。Mapper也支持,这种路由的注册
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
# 注册一个/hi路由 需要纯小写字母组成的project_id 并把controller设为say_hi
mapper.connect("/hi/{project_id:[a-z]+}", controller="say_hi")
# 查找/hi下 project_id=abc
m_result = mapper.routematch("/hi/abc")
print(m_result)
# 查找/hi下 project_id=abc2
m2_result = mapper.routematch("/hi/abc2")
print(m2_result)
从上面的代码也能看出来,很简单{project_id:[a-z]+},就是变量名+冒号+正则表达式。不支持:project_id:[a-z]+的。注意是区分大小写的。我们预测结果第一个匹配到,第二个匹配不到。
查看结果
({'controller': u'say_hi', 'project_id': u'abc'}, <routes.route.Route object at 0x0000000003D0A320>)
None
结果符合预期!
还有下面两种写法,例如
mapper.connect("/hi/{project_id}", controller="say_hi", requirements=dict(project_id=r"[a-z]+"))
mapper.connect("/hi/:project_id", controller="say_hi", requirements=dict(project_id=r"[a-z]+"))
就是前面只有变量。后面加个requirements,字典里限定变量的格式。
注册带请求方式的路由
一般情况下,我们需要不同的请求方式,交给不同的方法去处理,routes可不可以处理,答案是可以的,请看下面
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
mapper.connect("/hi", controller="say_hi", conditions=dict(method=["GET"]), action="list")
mapper.connect("/hi", controller="say_hi", conditions=dict(method=["POST"]), action="new")
m_result = mapper.routematch("/hi")
print(m_result)
m2_result = mapper.routematch(environ={"REQUEST_METHOD": "GET", "PATH_INFO": "/hi"})
print(m2_result)
m3_result = mapper.routematch(environ={"REQUEST_METHOD": "POST", "PATH_INFO": "/hi"})
print(m3_result)
注册路由的时候,加入一个conditions,值为一个字典,自带中包含一个method的key,对应的值为一个列表,就是允许的请求方式。这些都是固定的不能更改的。
我们第一次匹配是mapper.routematch("/hi")
,是我们上面熟悉的方式,其实routematch可以接收两个参数第一个url,是一个字符串,第二个参数叫做environ,熟悉WSGI的一定知道这是啥,不熟悉的话,它其实就是个字典,包含一些请求信息。可以看出无法从url中传递请求方式,所以只传入url的情况下,默认请求方式是GET。
所以后面两个测试是通过environ传递的值。不了解WSGI的话,只需要知道REQUEST_METHOD这个对应的值就是请求方式,PATH_INFO对应的就是请求URL就可以了。想用这个测试,该对应的值就好了。不过要说明environ要包含很多键值对,此处为了测试方便,只写了两个用到的key。所以不要认为就这两个就是个标准的environ了。
下面看结果
({'action': u'list', 'controller': u'say_hi'}, <routes.route.Route object at 0x00000000041AB2E8>)
({'action': u'list', 'controller': u'say_hi'}, <routes.route.Route object at 0x00000000041AB2E8>)
({'action': u'new', 'controller': u'say_hi'}, <routes.route.Route object at 0x00000000041AB278>)
我们从action区分,前两个匹配到GET,第三个匹配到POST,很符合语气。
进阶用法submapper
submapper用法一
我们有一堆的路由需要同一个controller处理,但是是不同的action我们可以怎么做呢?
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
sub_mapper = mapper.submapper(controller="handle_hobby")
sub_mapper.connect(None, "/hi/boy/{name}", action="game")
sub_mapper.connect(None, "/hi/girl/{name}", action="bag")
m_result = mapper.routematch("/hi/boy/jack")
print(m_result)
m2_result = mapper.routematch("/hi/girl/alina")
print(m2_result)
先用mapper生成一个submapper,传入一个公共属性,例如controller。
然后再用sub_mapper去注册子路由。
上面我们发现,在调用connect时第一个参数传入了None,第二个参数传入的才是路由。这里第一个参数,代表的是这条路由规则的名称,name。我们直接用mapper的时候并没有传name,是因为它在处理的时候,如果只传入了一个无名参数,会默认加上一个,None作为路由的名称。如果传入的参数超过一个,第一个作为路由名称,第二个作为路由规则。
我们看一下结果
({'action': u'game', 'controller': u'handle_hobby', 'name': u'jack'}, <routes.route.Route object at 0x00000000034CA278>)
({'action': u'bag', 'controller': u'handle_hobby', 'name': u'alina'}, <routes.route.Route object at 0x00000000034CA320>)
我们看到了,两个不同的action,相同的controller。
submapper用法二
我们一般应用中,如果需要同样的controller,也一般会有同样的url前缀,submapper也可以设置url前缀。就是设置一下path_prefix。下面是个示例代码
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
sub_mapper = mapper.submapper(path_prefix="/hi", controller="handle_hobby")
sub_mapper.connect("/boy/{name}", action="game")
sub_mapper.connect("/girl/{name}", action="bag")
m_result = mapper.routematch("/hi/boy/jack")
print(m_result)
m2_result = mapper.routematch("/hi/girl/alina")
print(m2_result)
可能有些人有疑问,上面刚说过,submapper在调用connect时必须传入一个name,为啥我上面看着像只传了路由规则,不想传了name。实际是什么样子呢。实际是,我传入的是name,路由规则是自动用的name的值。可能又有人疑问,为啥我在上面的用法一没有这么用。这么用有一个限制就是,必须设置path_prefix。如果没设置path_prefix。路由规则不会用name的值,会是None,运行的时候就会报错了。好奇小朋友可以试一下或者看一下源码就知道了。
看一下运行结果:
({'action': u'game', 'controller': u'handle_hobby', 'name': u'jack'}, <routes.route.Route object at 0x000000000422A278>)
({'action': u'bag', 'controller': u'handle_hobby', 'name': u'alina'}, <routes.route.Route object at 0x000000000422A320>)
这里需要注意的是:path_prefix和注册子路由的时候连接使用空字符连接的。也就是我path_prefix是/hi的情况,如果想拼接出/hi/boy/{name},子路由规则必须是/boy/{name}。如果写成boy/{name},就只能匹配到/hiboy/{name}。
submapper用法三
我们在上面提到了,我们可以调用connect时通过设置conditions来限制请求方式。一般情况下,我们一个路由会有多种请求方式,但一般我们交给同一个controller的不同action去处理。这里我们就可以通过submapper来更简洁的实现
# !/usr/bin/env python
# coding: utf-8
from routes.mapper import Mapper
# 实例化一个Mapper对象
mapper = Mapper()
sub_mapper = mapper.submapper(path_prefix="/hi", controller="handle_hi")
sub_mapper.action(action='show', method='GET')
sub_mapper.action(action='create', method='POST')
m_result = mapper.routematch(environ={"REQUEST_METHOD": "GET", "PATH_INFO": "/hi"})
print(m_result)
m2_result = mapper.routematch(environ={"REQUEST_METHOD": "POST", "PATH_INFO": "/hi"})
print(m2_result)
调用submapper的action方法,传入action和method即可。
看一下运行结果
({'action': u'show', 'controller': u'handle_hi', 'format': None}, <routes.route.Route object at 0x00000000043BA278>)
({'action': u'create', 'controller': u'handle_hi', 'format': None}, <routes.route.Route object at 0x00000000043BAC18>)
他还有更简洁的方法
sub_mapper.index() # = sub_mapper.action(action='index', method='GET')
sub_mapper.create() # = sub_mapper.action(action='create', method='POST')
sub_mapper.show() # = sub_mapper.action(action='show', method='GET')
sub_mapper.update() # = sub_mapper.action(action='update', method='PUT')
sub_mapper.delete() # = sub_mapper.action(action='delete', method='DELETE')
他等于内置了action和method。
测试时match不到路由
有时候你会发现明明应该match到的,就是match不到。例如
mapper = Mapper()
mapper.connect("/a", controller="handle_a")
m_result = mapper.routematch("/a")
print(m_result)
mapper.connect("/b", controller="handle_b")
m2_result = mapper.routematch("/b")
print(m2_result)
运行结果是:
({'controller': u'handle_a'}, <routes.route.Route object at 0x0000000003F56588>)
None
明明一样为啥/a能match到,/b不能match到。
Mapper在匹配路由前,会生成一个正则表达式,用于判断是否匹配到路由。
其实是你提交的路由(connect),只会保存起来,Mapper不会立刻生成或重新生成正则表达式,而是当你第一次调用routematch的时候再扫描所有保存的路由,进行正则表达式的生成,以备匹配路由。使用_created_regs这个标识为标识。一旦调用了route这个match_created_regs变成True。在这之后再调用connect其实只是保存了,但是不会再重新生成正则表达式,因此就不会匹配不上新添的路由。
两种解决方案:
1、测试的时候,写完所有connect再去用routematch,这样测试是没问题的
2、或者mapper = Mapper(always_scan=True)这样没次调用routematch的时候都会去重新生成正则表达式,但是只推荐自己学习的时候使用,生产环境可别用。