版本
通过路由中的name进行反向解析,drf跟Django中的使用并无不同!
但加上版本后,drf的版本类里也有个reverse反向解析的函数,它对Django的reverse进行了一点加工.
根据传递版本的三种方式,跟以往的方式进行反向解析相比,结果有些许的改变.
首先一点,就是结果中不仅有路径,还有协议、主机名等.
- 版本号在URL中传递参数 运用版本类的reverse 版本号会自动通过 ?version=v1 的方式拼接到反向解析的结果后面
- 版本号在路由参数中传递 运用版本类的reverse进行反向解析时 路由中关于版本的动态参数可以不传,会自动帮忙写
- 版本号在请求头中传递 暂时没发现啥不同,后续发现了再补充.
2
3
4
5
6
7
前言
在请求中携带版本号, 便于后续API的更新迭代!
后端跟前端(app‘ios/安卓’、小程序、网页)配合开发,前端向后端api发送请求,后端向前端返回json格式的数据.
同一个api会随着更新迭代有版本的不同,在开发过程中,版本不是一下子全部切换过去的,肯定会有两个版本同时存在的情况.
比如:小程序开发的快,网页开发的慢.. 同一个api都要对其进行支持,所以会有版本区分.
# 准备工作
准备工作: 创建指定版本的Django项目 - 安装djangorestframework - drf纯净版
解决认证组件匿名用户导致的报错,不用翻笔记的小技巧:
1> - APIView类里有很多默认的配置,点击跳转api_settings,可以看到配置名‘REST_FRAMEWORK’
- 匿名用户的key叫啥?回忆下,认证组件是放到Request对象里的.
APIView - dispatch - initialize_request - Request 或者 from rest_framework.request import Request
2> 认证权限限流的全局和局部配置的配置名可以去APIView类里找
比如: authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
(等号左边是局部的,右边是全局的!!!)
另外,authentication_classes结尾是es,表示是复数,那么其值是多个,局部和全局都用[];
若结尾没有es,局部和全局的其值就是一个字符串!
2
3
4
5
6
7
8
9
10
# 快速使用
传递版本的三种方式!
1> url的GET参数中传递 2>路由中传递参数 3>请求头中传递参数.
# URL中传递参数
from rest_framework.versioning import QueryParameterVersioning
此案例使用的是局部配置.
比如: http://127.0.0.1:8000/home/?version=2
后端会读取url中GET参数version的值,并将其赋值给 request.version!
url.py 根路由
from django.urls import path
from api import views
urlpatterns = [
path('home/', views.HomeView.as_view(), name='hh'),
]
2
3
4
5
6
settings.py
REST_FRAMEWORK = {
"UNAUTHENTICATED_USER": None,
"VERSION_PARAM": "xx", # 版本参数名
# "DEFAULT_VERSION": 0.1, # 默认版本号
# "ALLOWED_VERSIONS": ["2", "3", "4"],, # 允许的版本号
# "DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.QueryParameterVersioning"
}
# 已配置DEFAULT_VERSION和ALLOWED_VERSIONS
# url中没有GET参数xx,其值就是DEFAULT_VERSION;url中有GET参数xx,其值只能是ALLOWED_VERSIONS中的项.
# eg: 但凡url中有get参数xx 127.0.0.1:8000/home/?xx=.. xx的值只能是2、3、4
# - 127.0.0.1:8000/home/?xx=5 127.0.0.1:8000/home/?xx=0.1
# 报错 {"detail": "Invalid version in query parameter."}
# - 127.0.0.1:8000/home/ 不会报错, request.version值为0.1
# 127.0.0.1:8000/home/?xx=2 不会报错,request.version值为2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意: 读取url中GET参数version是默认的.. 可在全局进行配置读取url中的哪个参数, 但读取的值依旧会赋值给request.version..
api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import QueryParameterVersioning
from django.urls import reverse
class HomeView(APIView):
# 配置文件 VERSION_PARAM
# 1> http://127.0.0.1:8000/home/?xx=2 --> request.version 值为 2
# 2> settings.py中是否配置了"DEFAULT_VERSION": 0.1
# 已配置 http://127.0.0.1:8000/home/ --> request.version 值为 0.1
# 未配置 http://127.0.0.1:8000/home/ --> request.version 值为 None
versioning_class = QueryParameterVersioning
def get(self, request):
self.dispatch
print(request.version, type(request.version)) # 版本号
print(request.versioning_scheme) # 版本类的类实例化对象
drf_url = request.versioning_scheme.reverse('hh', request=request)
django_url = reverse('hh')
print("drf中反向生成url:", drf_url)
print("Django中反向生成url:", django_url)
return Response("...")
""" 浏览器输入网址: http://127.0.0.1:8000/home/?xx=3
3 <class 'str'>
<rest_framework.versioning.QueryParameterVersioning object at 0x7fd82de49ee0>
drf中反向生成url: http://127.0.0.1:8000/home/?xx=3
Django中反向生成url: /home/
"""
简单说一下,url中传递参数的反向解析,关键源码看这三行就理解了.
- version, scheme = self.determine_version(request, *args, **kwargs)
- request.version, request.versioning_scheme = version, scheme
- 在QueryParameterVersioning版本类中的reserve函数会
return replace_query_param(url, self.version_param, request.version)
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
# 路由参数中传递(*)
"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.URLPathVersioning"
此案例使用的是全局配置.
url.py 根路由
from django.urls import path, re_path
from api import views
urlpatterns = [
# ★ 等同于 re_path(r'^api/(?P<version>\w+)/home2/$', views.HomeView2.as_view(), name="hh2") 它两效果一样!
path('api/<str:version>/home/', views.HomeView1.as_view(), name='hh'),
]
# 注意:
# 1> 不建议用无名分组,因为使用无名分组
# 在使用drf的反向解析request.versioning_scheme.reverse('hh3', request=request)时会报错!!找不到version参数.
# 2> 上述的两种路由写法,路由中的参数 都会传递给视图函数的形参 **kwargs !!
# kwargs中可以取到,在drf中还可以通过self.kwargs取.
# 3> 格外提醒一点,path是完完全全的一模一样才能匹配上!!!
2
3
4
5
6
7
8
9
10
11
12
13
14
settings.py
REST_FRAMEWORK = {
"UNAUTHENTICATED_USER": None,
# "VERSION_PARAM": "version", # 其实该值不用设置,默认就是version,而且该值应该和路由中设置的参数的名字一样!!
"DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.URLPathVersioning"
}
2
3
4
5
api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
# from rest_framework.versioning import URLPathVersioning
from django.urls import reverse
class HomeView1(APIView):
# versioning_class = URLPathVersioning
"""
直接写参数version,可以直接拿到路由中传递的版本信息,但通常为了接受路由中的多个参数,我们都会写*args,**kwargs.
Q: ★那我们直接从kwagrs字典中取键version对应的值就行了啊,是不是不需要版本类啦?
A: ★使用drf的版本,它会将版本号封装到request中,这样我们在其他视图类中也可以通过request取到!
而且使用drf的版本后,进行反向解析时,会更方便.
"""
def get(self, request, *args, **kwargs):
print(args, kwargs)
print(request.version, type(request.version))
print('drf >>:', request.versioning_scheme.reverse('hh', request=request))
print('django >>:', reverse('hh', kwargs=kwargs))
return Response("...")
""" 浏览器输入: http://127.0.0.1:8000/api/2/home/
() {'version': '2'}
2 <class 'str'>
drf >>: http://127.0.0.1:8000/api/2/home/
django >>: /api/2/home/
"""
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
# Accept请求头
from rest_framework.versioning import AcceptHeaderVersioning
此案例使用的是局部配置!!
url.py 根路由
from django.urls import path
from api import views
urlpatterns = [
path('order/', views.OrderView.as_view(), name='yy1'),
]
2
3
4
5
6
api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import AcceptHeaderVersioning
from django.urls import reverse
class OrderView(APIView):
versioning_class = AcceptHeaderVersioning
# PostMan中GET请求输入 http://127.0.0.1:8000/order/
# 并设置请求头, Accept --> application/json;version=v1
def get(self, request, *args, **kwargs):
print(args, kwargs) # () {}
print(request.version) # v1
print(reverse('yy1')) # /order/
print(request.versioning_scheme.reverse('yy1', request=request)) # http://127.0.0.1:8000/order/
return Response('Hello World')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 反向解析
在每个版本处理的类中还定义了
reverse
方法, 它是用来反向生成URL并携带相关的的版本信息用的!简单来说, 使用Django的url反向解析, 需传递参数, 那么drf的url反向解析也需要传递..
首先明白一点, url的反向解析跟访问的哪个路由没有关系, (意思是我在路由A对应的视图函数中可以url解析B路由!!)
1> QueryParameterVersioning的版本参数是在 url的GET参数中 的 (?/version=v1)
So,无论是Django还是drf, 路由中有多少个参数反向解析时就传多少个. drf的url反向解析, 版本信息会自动 ?version=v1添加
2> URLPathVersioning的版本参数时在 路由中 的 (path('index/<int:pk>/<str:version>/',..,..
)
So,Django的url反向解析路由中有多少个就传多少个; drf的url反向解析, version参数会自动添加, 可以不传.其余的都需要传.
3> AcceptHeaderVersioning的版本参数在Accept请求头里, So, 情况跟QueryParameterVersioning一样!!
url.py 根路由
from django.urls import path, re_path
from api import views
urlpatterns = [
# 版本号在url中传递
# 注意:name为hh1和hh2的路由相应的视图类都是HomeView.
path('home/', views.HomeView.as_view(), name='hh1'),
path('home/<int:pk>/', views.HomeView.as_view(), name='hh2'),
# 版本号在路由参数中传递
path('index/<int:pk>/<str:version>/', views.IndexView.as_view(), name='ww1'),
# 版本号在请求头中传递
path('order/<int:pk>/', views.OrderView.as_view(), name='yy1'),
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
api/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import QueryParameterVersioning, URLPathVersioning, AcceptHeaderVersioning
from django.urls import reverse
class HomeView(APIView):
versioning_class = QueryParameterVersioning
def get(self, request, *args, **kwargs):
print(request.resolver_match)
print(args, kwargs)
print(request.version, type(request.version))
# !注意哦,使用路由反向解析,跟当前访问哪个api是无关的.
# 根据下方的实验,可以证明 在当前匹配的路由是可以反向解析其他路由的!!
# http://127.0.0.1:8000/home/?version=v1 匹配上的是 name为hh1的路由 “request.resolver_match”的值中表明了.
# http://127.0.0.1:8000/home/2/?version=v1 匹配上的是 name为hh2的路由
print()
print("=== 当前访问网址:", request.get_full_path())
print("django_hh1 >>:", reverse('hh1'))
print("django_hh2 >>:", reverse('hh2', kwargs={'pk': 11}))
print("drf_hh1 >>:", request.versioning_scheme.reverse('hh1', request=request))
print("drf_hh2 >>:", request.versioning_scheme.reverse('hh2', kwargs={'pk': 11}, request=request))
print("drf_hh2 >>:", request.versioning_scheme.reverse('hh2', args=(11,), request=request))
return Response('Hello World')
""" 浏览器输入: http://127.0.0.1:8000/home/?version=v1
ResolverMatch(func=api.views.HomeView, args=(), kwargs={}, url_name=hh1, app_names=[], namespaces=[], route=home/)
() {}
v1 <class 'str'>
=== 当前访问网址: /home/?version=v1
django_hh1 >>: /home/
django_hh2 >>: /home/11/
drf_hh1 >>: http://127.0.0.1:8000/home/?version=v1
drf_hh2 >>: http://127.0.0.1:8000/home/11/?version=v1
drf_hh2 >>: http://127.0.0.1:8000/home/11/?version=v1
"""
"""浏览器输入: http://127.0.0.1:8000/home/2/?version=v1
ResolverMatch(func=api.views.HomeView, args=(), kwargs={'pk': 2}, url_name=hh2, app_names=[], namespaces=[], route=home/<int:pk>/)
() {'pk': 2}
v1 <class 'str'>
=== 当前访问网址: /home/2/?version=v1
django_hh1 >>: /home/
django_hh2 >>: /home/11/
drf_hh1 >>: http://127.0.0.1:8000/home/?version=v1
drf_hh2 >>: http://127.0.0.1:8000/home/11/?version=v1
drf_hh2 >>: http://127.0.0.1:8000/home/11/?version=v1
"""
class IndexView(APIView):
versioning_class = URLPathVersioning
def get(self, request, *args, **kwargs):
print(request.resolver_match)
print(args, kwargs)
print(request.version, type(request.version))
print(reverse('ww1', kwargs=kwargs))
print(request.versioning_scheme.reverse('ww1', kwargs=kwargs, request=request))
# -- 此处就证明了version参数可以不传. 不传使用的是默认的.
print(request.versioning_scheme.reverse('ww1', kwargs={'pk': 11}, request=request))
return Response('Hello World')
"""浏览器输入: http://127.0.0.1:8000/index/1/v1/
ResolverMatch(func=api.views.IndexView, args=(), kwargs={'pk': 1, 'version': 'v1'}, url_name=ww1, app_names=[], namespaces=[], route=index/<int:pk>/<str:version>/)
() {'pk': 1, 'version': 'v1'}
v1 <class 'str'>
=== 当前访问网址: /index/1/v1/
django_ww1 >>: /index/1/v1/
drf_ww1 >>: http://127.0.0.1:8000/index/1/v1/
drf_ww1 >>: http://127.0.0.1:8000/index/11/v1/
"""
class OrderView(APIView):
versioning_class = AcceptHeaderVersioning
def get(self, request, *args, **kwargs):
print(args, kwargs)
print(request.version)
print()
print("=== 当前访问网址:", request.get_full_path())
print("django_yy1 >>:", reverse('yy1', kwargs={"pk": 98}))
print("drf_yy1 >>:", request.versioning_scheme.reverse('yy1', kwargs={"pk": 98}, request=request))
return Response('Hello World')
"""PostMan GET请求,http://127.0.0.1:8000/order/98/,并添加请求头Accept的值为application/json;version=v1
() {'pk': 98}
v1
=== 当前访问网址: /order/98/
django_yy1 >>: /order/98/
drf_yy1 >>: http://127.0.0.1:8000/order/98/
"""
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# 源码分析
此处源码分析的过程是以版本类 QueryParameterVersioning 为例的!!
版本类URLPathVersioning、AcceptHeaderVersioning 的执行流程都差不多..
最大的不同就是怎么取值的, 看各自类中的 determine_version 函数.
- QueryParameterVersioning -->
version = request.query_params.get(self.version_param, self.default_version)
- URLPathVersioning -->
version = kwargs.get(self.version_param, self.default_version)
- AcceptHeaderVersioning -->
version = media_type.params.get(self.version_param, self.default_version)
# -- 自己项目中的视图类
class HomeView(APIView):
versioning_class = QueryParameterVersioning
def get(self, request):
print(request.version)
return Response("...")
# -- rest_framework/views.py
class APIView(View):
# api_settings.DEFAULT_VERSIONING_CLASS drf对其设置的默认值是None.
# 再次敲黑板, versioning_class的查找顺序: 视图类中的局部配置 - 项目配置文件中的全局配置 - drf配置文件中默认的
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
def initial(self, request, *args, **kwargs):
# 版本控制
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme # 版本号, 《版本类的类实例化对象》
self.perform_authentication(request) # 认证
self.check_permissions(request) # 权限
self.check_throttles(request) # 限流
def determine_version(self, request, *args, **kwargs):
if self.versioning_class is None:
return (None, None)
# 版本类的类实例化对象,此处是 QueryParameterVersioning()
# 条件反射/看到类实例化就要想到,看其__init__和父类的__init__,看源码,没有做任何初始化的操作!
scheme = self.versioning_class()
# 提醒: scheme.determine_version(request, *args, **kwargs) *args, **kwargs 是在路由中传递的参数
# eg:path("user/<int:id>/",views.UserView.as_view())
return (scheme.determine_version(request, *args, **kwargs), scheme)
# -- rest_framework/versioning.py
class BaseVersioning:
default_version = api_settings.DEFAULT_VERSION # 默认版本号 drf配置文件中DEFAULT_VERSION默认值为None
allowed_versions = api_settings.ALLOWED_VERSIONS # 允许的版本号
version_param = api_settings.VERSION_PARAM # 版本参数名
def determine_version(self, request, *args, **kwargs):
msg = '{cls}.determine_version() must be implemented.'
raise NotImplementedError(msg.format(
cls=self.__class__.__name__
))
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
return _reverse(viewname, args, kwargs, request, format, **extra)
def is_allowed_version(self, version):
"""判断版本号是否合法"""
if not self.allowed_versions:
return True
# 理解下该逻辑.
# 当全局配置了VERSION_PARAM='xx'、DEFAULT_VERSION、ALLOWED_VERSIONS
# url中没有GET参数xx,其值就是DEFAULT_VERSION;url中有GET参数xx,其值只能是ALLOWED_VERSIONS中的项.
return ((version is not None and version == self.default_version) or
(version in self.allowed_versions))
class QueryParameterVersioning(BaseVersioning):
"""
GET /something/?version=0.1 HTTP/1.1
Host: example.com
Accept: application/json
"""
invalid_version_message = _('Invalid version in query parameter.')
def determine_version(self, request, *args, **kwargs):
# request.query_params.get 从url中取值
# self.version_param url中的GET版本参数,self.default_version默认版本值 看父类BaseVersioning
# 再次提醒查找顺序: 因为该self不是视图类的实例化对象,所以此处的顺序是先项目配置文件再drf配置文件.
# 分析结果: version要么是传入的,要么是默认的.
version = request.query_params.get(self.version_param, self.default_version) # url中没传就用默认的
# self.is_allowed_version(version) 判断该版本号是否合法!!
if not self.is_allowed_version(version):
# 该异常最终会在类APIView的dispatch中被捕获到.然后返回.
raise exceptions.NotFound(self.invalid_version_message)
return version
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
# 调用父类BaseVersioning中的reverse
# 本质就是使用 Django中的reverse,根据name反向生成url!
url = super().reverse(
viewname, args, kwargs, request, format, **extra
)
if request.version is not None:
# ★不细看replace_query_param啦,简单来说就是在反向生成的url后面加上 ?version=2 即反向生成的url携带版本号!
return replace_query_param(url, self.version_param, request.version)
return url
# -- rest_framework/reverse.py
def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
if format is not None:
kwargs = kwargs or {}
kwargs['format'] = format
# 本质就是调用Django中的reverse!! from django.urls import reverse as django_reverse
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
if request:
return request.build_absolute_uri(url)
return url
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111