rest framework之版本控制
一、版本控制的使用
版本控制允许在不同的客户端之间更改行为,其实质就是后台根据客户端传递过来的版本信息做相应的动作,比如不同版本对应不同的序列化样式:
def get_serializer_class(self): if self.request.version == 'v1': return UserSerializerVersion1 return UserSerializer
(一)URLPathVersioning
1、在settings中配置DEFAULT_VERSIONING_CLASS
REST_FRAMEWORK = { "DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.URLPathVersioning", }
DEFAULT_VERSIONING_CLASS除非显式设置否则
将会是None
。在这种情况下request.version
属性将总是返回None
。当然还可以在单个视图上设置版本控制方案。通常不需要这样做,因为全局使用单一版本控制方案更有意义。如果确实需要这样做,请在视图类中使用versioning_class
属性。
versioning_class = MyVersioning #非元祖或者列表
2、其它配置参数
(1)DEFAULT_VERSION
. 当版本控制信息不存在时用于设置request.version
的默认值,默认设置为None
。
(2)ALLOWED_VERSIONS
. 如果设置了此值将限制版本控制方案可能返回的版本集,如果客户端请求提供的版本不在此集中,则会引发错误。请注意,用于DEFAULT_VERSION
的值应该总是在ALLOWED_VERSIONS
设置的集合中(除非是None
)。该配置默认是 None
。
(3)VERSION_PARAM
. 一个应当用于任何版本控制系统参数的字符串,例如媒体类型或URL查询参数。默认值是'version'
3、完整配置
REST_FRAMEWORK = { "DEFAULT_VERSIONING_CLASS":"rest_framework.versioning.URLPathVersioning", "DEFAULT_VERSION":'v1', "ALLOWED_VERSIONS":['v1','v2'], "VERSION_PARAM":'version', }
4、路由配置
re_path('(?P<version>[v1|v2]+)/books/$', views.BookView.as_view(), name="books"),
5、视图中获取版本信息
from rest_framework.generics import ListAPIView class BookView(ListAPIView): def get(self, request, *args, **kwargs): #获取版本 version = request.version #获取处理版本对象 version_obj = request.versioning_scheme #可以使用处理版本对象反向生成url,传递request是为了传递version参数 url = request.versioning_scheme.reverse(viewname='books', request=request) return HttpResponse('...')
(二)其它API
1、QueryParameterVersioning
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): version = request.query_params.get(self.version_param, self.default_version) if not self.is_allowed_version(version): raise exceptions.NotFound(self.invalid_version_message) return version def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): url = super(QueryParameterVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) if request.version is not None: return replace_query_param(url, self.version_param, request.version) return url
此方案是一种在 URL 中包含版本信息作为查询参数的简单方案,路由中没有对参数进行分组:
re_path('books/$', views.BookView.as_view(), name="books"),
访问时需要传递参数:
http://127.0.0.1:8020/books/?version=v1
后台通过query_params参数来获取:
class BookView(ListAPIView): def get(self, request, *args, **kwargs): #获取版本 version = request.query_params.get('version') return HttpResponse('...')
当然,这只是它实现的大概原理,如果使用这种版本控制方式只需要像URLPathVersioning进行配置即可。
2、NamespaceVersioning
class NamespaceVersioning(BaseVersioning): """ To the client this is the same style as `URLPathVersioning`. The difference is in the backend - this implementation uses Django's URL namespaces to determine the version. An example URL conf that is namespaced into two separate versions # users/urls.py urlpatterns = [ url(r'^/users/$', users_list, name='users-list'), url(r'^/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail') ] # urls.py urlpatterns = [ url(r'^v1/', include('users.urls', namespace='v1')), url(r'^v2/', include('users.urls', namespace='v2')) ] GET /1.0/something/ HTTP/1.1 Host: example.com Accept: application/json """ invalid_version_message = _('Invalid version in URL path. Does not match any version namespace.') def determine_version(self, request, *args, **kwargs): resolver_match = getattr(request, 'resolver_match', None) if resolver_match is None or not resolver_match.namespace: return self.default_version # Allow for possibly nested namespaces. possible_versions = resolver_match.namespace.split(':') for version in possible_versions: if self.is_allowed_version(version): return version raise exceptions.NotFound(self.invalid_version_message) def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: viewname = self.get_versioned_viewname(viewname, request) return super(NamespaceVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) def get_versioned_viewname(self, viewname, request): return request.version + ':' + viewname
对于客户端,此方案与URLPathVersioning
相同。唯一的区别是,它是如何在 Django 应用程序中配置的,因为它使用URL conf中的命名空间而不是URL conf中的关键字参数。
使用此方案,request.version
属性是根据与传入请求的路径匹配的 namespace
确定的,如下实例:
urlpatterns = [ url(r'^v1/books/', include('books.urls', namespace='v1')), url(r'^v2/books/', include('books.urls', namespace='v2')) ]
3、HostNameVersioning
class HostNameVersioning(BaseVersioning): """ GET /something/ HTTP/1.1 Host: v1.example.com Accept: application/json """ hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$') invalid_version_message = _('Invalid version in hostname.') def determine_version(self, request, *args, **kwargs): hostname, separator, port = request.get_host().partition(':') match = self.hostname_regex.match(hostname) if not match: return self.default_version version = match.group(1) if not self.is_allowed_version(version): raise exceptions.NotFound(self.invalid_version_message) return version # We don't need to implement `reverse`, as the hostname will already be # preserved as part of the REST framework `reverse` implementation.
主机名版本控制方案要求客户端将请求的版本指定为URL中主机名的一部分。 例如,以下是对http://v1.example.com/books/
的HTTP请求:
GET /books/ HTTP/1.1
Host: v1.example.com
Accept: application/json
默认情况下,此实现期望主机名与以下简单正则表达式匹配:
^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$
二、源码剖析
(一)URLPathVersioningAPI源码
1、dispatch
def dispatch(self, request, *args, **kwargs): """ `.dispatch()` is pretty much the same as Django's regular dispatch, but with extra hooks for startup, finalize, and exception handling. """ self.args = args self.kwargs = kwargs #rest-framework重构request对象 request = self.initialize_request(request, *args, **kwargs) self.request = request self.headers = self.default_response_headers # deprecate? try: self.initial(request, *args, **kwargs) # Get the appropriate handler method #这里和CBV一样进行方法的分发 if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed response = handler(request, *args, **kwargs) except Exception as exc: response = self.handle_exception(exc) self.response = self.finalize_response(request, response, *args, **kwargs) return self.response
在APIView中的dispatch方法中进行了原request的封装以及其它组件的添加,所以这是rest framework功能的起点,在initial方法中含有与版本相关的内容。
2、initial
def initial(self, request, *args, **kwargs): """ Runs anything that needs to occur prior to calling the method handler. """ ... # Determine the API version, if versioning is in use. #与版本相关 version, scheme = self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme = version, scheme ...
在这里调用了self.determine_version,这里的self指的是APIView对象,返回值就是对应的版本以及版本对象,并且将版本以及版本对象赋值给了request对象,在self.determine_version方法中调用了配置中版本类对象的determine_version方法。
3、determine_version
这里的determine_version指的是版本类中的方法,加入配置的版本API是URLPathVersioning:
class URLPathVersioning(BaseVersioning): """ To the client this is the same style as `NamespaceVersioning`. The difference is in the backend - this implementation uses Django's URL keyword arguments to determine the version. An example URL conf for two views that accept two different versions. urlpatterns = [ url(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'), url(r'^(?P<version>[v1|v2]+)/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail') ] GET /1.0/something/ HTTP/1.1 Host: example.com Accept: application/json """ invalid_version_message = _('Invalid version in URL path.') def determine_version(self, request, *args, **kwargs): version = kwargs.get(self.version_param, self.default_version) if version is None: version = self.default_version if not self.is_allowed_version(version): raise exceptions.NotFound(self.invalid_version_message) return version ...
在这里通过kwargs获取分组参数的值,而键值是配置文件中配置好的,如果没有就是用默认配置键version。
(二)反向生成url
可以看到在每一个API中都有url反向生成的函数:
class URLPathVersioning(BaseVersioning): ... def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: kwargs = {} if (kwargs is None) else kwargs kwargs[self.version_param] = request.version return super(URLPathVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) ...
在这个函数当中,会把获取到的版本值放入kwargs中去,然后调用父类的reverse方法进行解析,这与django中的reverse相似,只不过reverse后面的参数是版本通过传递的request进行处理的:
reverse(viewname="books",kwargs={'version':kwargs["version"]})
参考文档:https://q1mi.github.io/Django-REST-framework-documentation/api-guide/versioning_zh/