前言
API版本控制是一个关键的实践,它有助于确保API的稳定性、可靠性和兼容性,同时为API的长期维护和演进提供支持。通过在请求中携带版本号,开发者可以更好地管理API的不同版本,并确保对客户端的影响最小化
1. GET参数传递
配置文件中的
VERSION_PARAM
决定哪个参数表示版本信息参数携带的版本信息会被封装到
request.version
settings.py中和版本组件有关的参数
REST_FRAMEWORK = {
# 携带版本信息的请求参数名
'VERSION_PARAM': 'version',
# 未传入版本参数时的默认版本
'DEFAULT_VERSION': "v1",
# 版本范围列表
'ALLOWED_VERSIONS': ["v1", "v2"],
# 默认版本类
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.QueryParameterVersioning',
}
如图,URL参数中传递version=v1,则request.version存储了版本信息v1
2. 反向生成URL
与django中的反向生成URL不同,drf反向生成的URL可以携带相关版本信息
将路由的name传入,并且将request传入(版本信息在request中)即可反向生成URL
3. 源码解读
入口在APIView类的initial()方法
class BaseVersioning:
default_version = api_settings.DEFAULT_VERSION
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是默认值或者在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):
# 去请求参数中获取键为version_param的值即为版本,若不存在则读取settings.py中的默认值
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
# 传入视图名称和request
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
# 本质上就是调用django的reverse反向生成URL
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
def replace_query_param(url, key, val):
(scheme, netloc, path, query, fragment) = parse.urlsplit(force_str(url))
query_dict = parse.parse_qs(query, keep_blank_values=True)
query_dict[force_str(key)] = [force_str(val)]
query = parse.urlencode(sorted(query_dict.items()), doseq=True)
return parse.urlunsplit((scheme, netloc, path, query, fragment))
class APIView(View):
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
def determine_version(self, request, *args, **kwargs):
if self.versioning_class is None:
return (None, None)
# 实例化类对象,即QueryParameterVersioning(),,欸有__init__方法
scheme = self.versioning_class()
# 返回版本信息与版本类对象
return (scheme.determine_version(request, *args, **kwargs), scheme)
def initial(self, request, *args, **kwargs):
# 将版本信息与版本类封装到request中
version, scheme = self.determine_version(request, *args, **kwargs)
request.version, request.versioning_scheme = version, scheme
# Ensure that the incoming request is permitted
self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)
def dispatch(self, request, *args, **kwargs):
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
handler = getattr(self, request.method.lower(), 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
class HomeView(APIView):
# 配置文件 VERSION_PARAM
# http://127.0.0.1:8000/home/?xx=123&page=44&vertion=v1 -> request.version
versioning_class = QueryParameterVersioning
def get(self, request):
print(request.version)
return Response("...")
4. URL路径传递
urls.py
中path和re_path两种方式均可
# 源码
class URLPathVersioning(BaseVersioning):
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
中读取版本,因此视图函数要用*args
和**kwargs
接收,其他源码逻辑与GET参数传递基本相同
5. Accept请求头传递
反向生成的URL不含版本
参考代码
# urls.py
from django.urls import path, re_path
from api import views
urlpatterns = [
# path('admin/', admin.site.urls),
path('home/', views.HomeView.as_view(), name='hm'),
# path('api/<str:version>/home/', views.HomeView.as_view(), name='hm'),
# re_path(r'^api/(?P<version>\w+)/home/$', views.HomeView.as_view(), name='hm'),
]
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.versioning import QueryParameterVersioning, URLPathVersioning, AcceptHeaderVersioning
# Create your views here.
class HomeView(APIView):
# 配置文件 VERSION_PARAM
# http://127.0.0.1:8000/home/?xx=123&page=44&vertion=v1 -> request.version
versioning_class = AcceptHeaderVersioning
def get(self, request, *args, **kwargs):
print(request.version)
print(request.versioning_scheme)
url = request.versioning_scheme.reverse('hm', request=request)
print("反向生成的url:", url)
return Response("...")
6. 总结
后续开发时,一般都采用URL路径传递的形式,下列给出开发示例
settings.py进行全局配置
REST_FRAMEWORK = {
'UNAUTHENTICATED_USER': None,
'VERSION_PARAM': 'version',
'DEFAULT_VERSION': "v1",
'ALLOWED_VERSIONS': ["v1", "v2"],
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
}
views.py
from rest_framework.views import APIView
from rest_framework.response import Response
class HomeView(APIView):
def get(self, request, *args, **kwargs):
print(request.version)
print(request.versioning_scheme)
url = request.versioning_scheme.reverse('hm', request=request)
print("反向生成的url:", url)
return Response("...")
urls.py
from django.urls import path, re_path
from api import views
urlpatterns = [
path('api/<str:version>/home/', views.HomeView.as_view(), name='hm'),
]