Fork me on GitHub

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
QueryParameterVersioning

此方案是一种在 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
NamespaceVersioning

对于客户端,此方案与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.
HostNameVersioning

主机名版本控制方案要求客户端将请求的版本指定为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/

 

posted @ 2019-09-14 10:13  iveBoy  阅读(484)  评论(0编辑  收藏  举报
TOP