API 版本控制允許我們在不同的客戶端之間更改行為(同一個接口的不同版本會返回不同的數據)。 DRF提供了許多不同的版本控制方案。
可能會有一些客戶端因為某些原因不再維護了,但是我們后端的接口還要不斷的更新迭代,這個時候通過版本控制返回不同的內容就是一種不錯的解決方案。
DRF提供的版本控制方案
DRF提供了五種版本控制方案,如下圖:
?
版本控制系統的使用
全局配置
這里我們以?URLPathVersioning 為例,還是在項目的settings.py中REST_FRAMEWORK配置項下配置:
REST_FRAMEWORK = {...'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning','DEFAULT_VERSION': 'v1', # 默認的版本'ALLOWED_VERSIONS': ['v1', 'v2'], # 有效的版本'VERSION_PARAM': 'version', # 版本的參數名與URL conf中一致
}
局部配置
注意,通常我們是不會單獨給某個視圖設置版本控制的,如果你確實需要給單獨的視圖設置版本控制,你可以在視圖中設置versioning_class屬性,如下:
class PublisherViewSet(ModelViewSet):...versioning_class = URLPathVersioning
urls.py
urlpatterns = [...url(r'^(?P<version>[v1|v2]+)/publishers/$', views.PublisherViewSet.as_view({'get': 'list', 'post': 'create'})),url(r'^(?P<version>[v1|v2]+)/publishers/(?P<pk>\d+)/$', views.PublisherViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),]
我們在視圖中可以通過訪問?request.version?來獲取當前請求的具體版本,然后根據不同的版本來返回不同的內容:
我們可以在視圖中自定義具體的行為,下面以不同的版本返回不同的序列化類為例
class PublisherViewSet(ModelViewSet):def get_serializer_class(self):"""不同的版本使用不同的序列化類"""if self.request.version == 'v1':return PublisherModelSerializerVersion1else:return PublisherModelSerializerqueryset = models.Publisher.objects.all()
REST framework 提供的默認版本的源碼
############################## versioning.py #############################
# coding: utf-8
from __future__ import unicode_literalsimport refrom django.utils.translation import ugettext_lazy as _from rest_framework import exceptions
from rest_framework.compat import unicode_http_header
from rest_framework.reverse import _reverse
from rest_framework.settings import api_settings
from rest_framework.templatetags.rest_framework import replace_query_param
from rest_framework.utils.mediatypes import _MediaType# 基礎類,其他類要繼承此類
class BaseVersioning(object):default_version = api_settings.DEFAULT_VERSION # 配置文件中獲取相應信息allowed_versions = api_settings.ALLOWED_VERSIONSversion_param = api_settings.VERSION_PARAMdef 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 Truereturn ((version is not None and version == self.default_version) or(version in self.allowed_versions))# 版本信息在頭部
class AcceptHeaderVersioning(BaseVersioning):"""GET /something/ HTTP/1.1Host: example.comAccept: application/json; version=1.0"""invalid_version_message = _('Invalid version in "Accept" header.')def determine_version(self, request, *args, **kwargs):media_type = _MediaType(request.accepted_media_type)version = media_type.params.get(self.version_param, self.default_version)version = unicode_http_header(version)if not self.is_allowed_version(version):raise exceptions.NotAcceptable(self.invalid_version_message)return version# We don't need to implement `reverse`, as the versioning is based# on the `Accept` header, not on the request URL.# 版本信息在url中
class URLPathVersioning(BaseVersioning):"""To the client this is the same style as `NamespaceVersioning`.The difference is in the backend - this implementation usesDjango'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.1Host: example.comAccept: 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_versionif not self.is_allowed_version(version):raise exceptions.NotFound(self.invalid_version_message)return versiondef reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):if request.version is not None:kwargs = {} if (kwargs is None) else kwargskwargs[self.version_param] = request.versionreturn super(URLPathVersioning, self).reverse(viewname, args, kwargs, request, format, **extra)class NamespaceVersioning(BaseVersioning):"""To the client this is the same style as `URLPathVersioning`.The difference is in the backend - this implementation usesDjango's URL namespaces to determine the version.An example URL conf that is namespaced into two separate versions# users/urls.pyurlpatterns = [url(r'^/users/$', users_list, name='users-list'),url(r'^/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')]# urls.pyurlpatterns = [url(r'^v1/', include('users.urls', namespace='v1')),url(r'^v2/', include('users.urls', namespace='v2'))]GET /1.0/something/ HTTP/1.1Host: example.comAccept: 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 versionraise 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 + ':' + viewnameclass HostNameVersioning(BaseVersioning):"""GET /something/ HTTP/1.1Host: v1.example.comAccept: 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_versionversion = 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參數區分
class QueryParameterVersioning(BaseVersioning):"""GET /something/?version=0.1 HTTP/1.1Host: example.comAccept: 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 versiondef 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
?