image-20231012220431847

API版本

在开发过程中可能会有多版本的API,因此需要对API进行管理。django drf中对于版本的管理也很方便。

http://www.example.com/api/v1/info

http://www.example.com/api/v2/info

上面这种形式就是很常见的版本管理

在restful规范中,后端的API需要体现出版本

在django drf中,共有三种形式的版本管理

  1. 通过GET参数传递
  2. 通过URL路由进行传递
  3. 通过请求头进行传递

下面将对这三种方法逐一介绍

1、通过get请求传递版本信息

视图函数代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import QueryParameterVersioning


class HomeView(APIView):
versioning_class = QueryParameterVersioning
def get(self, request):
print("api_version=", request.version)
print(request.versioning_scheme)
url = request.versioning_scheme.reverse("home", request=request)
print("drf反向生成的URL为", url)
self.dispatch
return Response({"code": 123123, "Home": "Home"})

urls.py

1
2
3
4
5
6
from django.urls import path
from app01.views import HomeView

urlpatterns = [
path("home/", HomeView.as_view(), name="home"),
]

在项目的settings.py文件中,我们还需要进行三个配置

1
2
3
4
5
6
7
8
REST_FRAMEWORK = {
# get请求的默认参数名称
"VERSION_PARAM": "version",
# 默认的版本
"DEFAULT_VERSION": "v1",
# 允许的版本
"ALLOWED_VERSIONS": ["v1", "v2", "v3", "v111"]
}

其中,VERSION_PARAM代表get请求的默认参数名,后面请求接口http://127.0.0.1:8000/home/?version=v1时,django会自动将参数中的version获取到,并赋值到request.version中。

具体的可以参考drf中通过get请求传递版本对应部分的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
# 用来方向生成URL
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
url = super().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类继承自BaseVersioing类。在BaseVersioing类中,有三个默认的定义:

1
2
3
4
class BaseVersioning:
default_version = api_settings.DEFAULT_VERSION
allowed_versions = api_settings.ALLOWED_VERSIONS
version_param = api_settings.VERSION_PARAM

default_version 代表默认的版本,会自动去全局配置中寻找,如果全局中没有配置则去局部进行寻找

allowed_versions 代表允许的版本号,会自动取settings.py文件中去读取相应的配置

version_param 代表get请求参数中的关键字,例如http://127.0.0.1:8000/home/?version=v1 例如,若settings.py文件中配置了此url中的version_param值为version,那么version就是获取版本的关键字(本质上是字典的键)

接下来看效果演示,这里通过postman来模拟get和post请求。

  • 首先,这里我没有在URL中携带版本信息,由于我在全局配置中写了默认是v1,并且关键字是version,因此会帮我按照这个配置信息反向生成一个url链接。

  • 接下来的请求我会携带version参数,可以看到响应成功,输出api版本信息并反向生成了url

  • 接下来我换一个关键词使用?xx=v3,drf会按照之前的配置,默认使用v1参数

  • 接下来我会使用不在ALLOWED_VERSIONS中的版本信息,v1000,程序报错,不合格的版本信息

image-20231009211750100

2、通过url路由传递版本信息

URL路由中携带版本(*)公司中常用的方法 可以直接将其写到settings.py文件中,这样可以方便后续使用,直接去全局配置中找到即可。

视图类代码

1
2
3
4
5
6
7
8
9
10
class Home2View(APIView):
versioning_class = URLPathVersioning

def get(self, request, *args, **kwargs):
print("api_version=", request.version)
print(request.versioning_scheme)
url = request.versioning_scheme.reverse("home2", request=request)
print("drf_HOME2反向生成的URL为", url)
self.dispatch
return Response({"code": 222222, "Home": "Home2"})

这里注意,反向生成URL的时候需要加上一个name

urls.py

1
2
3
4
5
6
from django.urls import path
from app01.views import Home2View

urlpatterns = [
path("api/<str:version>/home2/", Home2View.as_view(), name="home2"),
]

在类视图的代码中,我们将versioning_class 赋值为URLPathVersioning

在drf的源代码中,URLPathVersioning是这样实现的:

1
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
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 = [
re_path(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'),
re_path(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
# 如果是不合法的API版本,则抛出异常
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):
if request.version is not None:
kwargs = {} if (kwargs is None) else kwargs
kwargs[self.version_param] = request.version

return super().reverse(
viewname, args, kwargs, request, format, **extra
)

本质上就是一个determin_version()函数,首先会从**kwargs中获取有没有版本,如果没有版本信息则将版本赋值为全局配置中的默认版本。如果API是非法的,则抛出异常。

3、通过请求头传递

视图类:

1
2
3
4
5
6
7
8
9
10
class Home3View(APIView):
versioning_class = AcceptHeaderVersioning

def get(self, request, *args, **kwargs):
print("api_version=", request.version)
print(request.versioning_scheme)
url = request.versioning_scheme.reverse("home3", request=request)
print("drf_HOME2反向生成的URL为", url)
self.dispatch
return Response({"code": 333333, "Home": "Home3"})

urls.py

1
2
3
4
5
6
from django.urls import path
from app01.views import Home3View

urlpatterns = [
path("api/home3/", Home3View.as_view(), name="home3"),
]

在类视图的代码中,我们将versioning_class 赋值为AcceptHeaderVersioning

在drf的源代码中,AcceptHeaderVersioning是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 基于请求头传递版本信息
class AcceptHeaderVersioning(BaseVersioning):
"""
GET /something/ HTTP/1.1
Host: example.com
Accept: 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.

这个请求无法再浏览器中模拟,只能通过postman中去添加请求头进行模拟,

Headers中的KEY为Accept,VALUE为application/json; version=1.0

  • 这里我先将version设置为1.0,可以看到显示信息为不合法的版本信息

image-20231009213432844

  • 然后我将version设置为v3,可以看到可以正常访问

image-20231009213714147

如果我不在请求头中设置的话,默认还是会访问v1版本的API,这是由于我在全局中的配置所导致的。