作者:Kuky_xs
博客:https://www.jianshu.com/u/9fcd71535294
前言
系列文章:
在《用django写接口(入门篇)》提到这篇会讲 views 的代码优化,在这之前,我们先适当了解下 DRF 中的 Request 和 Response。
了解完 Request 和 Response 我们将分别通过 @api_view,APIView 和通用视图类对 view 进行一些改造
api_view 注解重构
# ....import 省略
# 将该视图的请求方法写在注解中,表示该接口只接受列表内的请求方式
@api_view(['GET', 'POST'])
def post_list(request):
if request.method == 'GET':
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
# 通过 Response 展示相应的数据
return Response(serializer.data)
elif requets.method == 'POST':
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
# 引入 status 模块,比数字标识符更加直观
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
然后运行项目,输入网址后,所展示的界面和之前的不同了,多了以下部分
HTTP 200 OK
Allow: GET, POST, OPTIONS
Content-Type: application/json
Vary: Accept
这些返回项,而且页面也不是 json了
优化后的列表接口信息
我们继续做一些修改,在 post_list 函数中加入 format 参数,默认值设置为 None,接着我们对 url 也做一些修改,通过 format_suffix_patterns 函数对接口返回的信息进行一些处理
# ....import 省略
app_name = 'api'
urlpatterns = [
# url(r'^posts/$', views.post_list, name='api_posts'),
url(r'^post/(?P<pk>[0-9]+)/$', views.post_detail, name='api_post'),
]
# 增加这一行,这样我们就不需要逐一地添加对格式支持的 url 样式
urlpatterns = format_suffix_patterns(urlpatterns)
然后我们对我们接口请求的网址做些修改,在我们之前请求的网址末尾加入 .json 记得去除最末尾的 "/",然后我们又可以看到修改前返回的 json 格式数据啦(这边就不贴图啦~)。对于 detail 接口的修改我们也可以根据对 list 的修改进行相应修改,不做多余解释。
APIView 重构
# ....import 省略
class PostList(APIView):
# 定义 GET 请求的方法,内部实现相同 @api_view
def get(self, request, format=None):
posts = Post.objects.all()
serializer = PostSerializer(posts, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
# 定义 POST 请求的方法
def post(self, requets, format=None):
serializer = PostSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
然后对 url 进行部分修改即可
urlpatterns = [
url(r'^posts/$', views.PostList.as_view(), name='posts'),
]
urlpatterns = fromat_suffix_patterns(urlpatterns)
然后我们又可以看到和上一个例子一样的界面
通过 mixins 和 generics 重构
class PostListMixins(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
# 指定列表
queryset = Post.objects.all()
# 指定序列化类
serializer_class = PostSerializer
def get(self, request, *args, **kwargs):
# list 方法继承 ListModelMixin 而来
return self.list(self, request, *args, **kwargs)
def post(self, request, *args, **kwargs):
# create 方法继承 CreateModelMixin 而来
return self.create(self, request, *args, **kwargs)
# detail 视图通过 mixins 和 generics 改造
class PostDetailMixin(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
def get(self, request, *args, **kwargs):
return self.retrieve(self, request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(self, request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(self, request, *args, **kwargs)
通用的基于类重构
# 列表视图
class PostListView(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
# detail 视图
class PostDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
重构 view,重构 url
首先我们通过继承 ViewSet 来重构 view 类
class PostViewSet(viewset.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
# 推荐重写该方法,默认返回 status.HTTP_204_NO_CONTENT,
# 会返回空信息,个人觉得不方便判断,当然按照个人喜好决定
def destroy(self, request, *args, **kwargs):
post = self.get_object()
if post is not None:
post.delete()
return Response({"message": "delete succeed", "code": "200"},
status=status.HTTP_200_OK)
return super(PostViewSet, self).destroy(self, request, *args, **kwargs)
然后我们通过 Routers 来重构 url
from rest_framework import routers
router = routers.DefaultRouter()
router.register(r'posts', view.PostViewSet)
router.register(r'post', view.PostViewSet)
# 然后我们需要在 project 下的 url 中去配置 router
from blog_api.urls import router as blog_api_router
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^api/', include(blog_api_router.urls)),
]
真是人比人,气死人,代码居然到最后少了那么多,至于为什么,我们来看下 ModelViewSet 的源码吧(兄 dei 别排斥源码啊,这里真的很少很少的,但是又能让我们知道到底做了什么事)
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
看到这是不是,觉得我们之前的优化都是一步接着一步来的,那我们继续看下 ModelMixin 的源码好了
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
# 根据上传的参数,判断是否有效的 serializer
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 保存 serializer
self.perform_create(serializer)
# 返回响应头信息
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': str(data[api_settings.URL_FIELD_NAME])}
except (TypeError, KeyError):
return {}
class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# 分页
page = self.paginate_queryset(queryset)
if page is not None:
# 返回该页的列表
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
# 没有分页则全部展示
serializer = self.get_serializer(queryset, many=True)
# 渲染 json
return Response(serializer.data)
class RetrieveModelMixin(object):
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
# 根据回传的 id 查找
instance = self.get_object()
# 将 instance 渲染成 json 展示
serializer = self.get_serializer(instance)
return Response(serializer.data)
class UpdateModelMixin(object):
"""
Update a model instance.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
# 更新 instance 的值
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
# 保存更新后的数据
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
class DestroyModelMixin(object):
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
# 删除数据
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
instance.delete()
其实内部的具体实现还是我们上一部分写的那些东西,接着,我觉得有必要把自己在 Android 端做的接口测试代码和运行结果贴出来,不然你们又会觉得我坑你们了......这边我为了偷懒(嗯对的,就是偷懒),我又写了一个只有单个字段的 model
Android 端 api
获取列表
获取列表结果
新建数据
新建数据返回结果
获取详情
获取详情返回结果
更新详情
更新详情返回结果
删除数据
删除数据返回结果
有坑!
在结束文章的最后,记录自己写的时候遇到的一个坑,当更新 ManyToMany 字段的时候,我们需要重新写 post 方法,直接传 id 是不能更新的,直接传 id 是不能更新的,直接传 id 是不能更新的(三遍三遍三遍)。
# 假设我们的 post 有一个 ManyToMany 字段 tags
class PostDetailView(APIView):
# 更新的时候,需要约定好 ManyToMany 字段的 id 回传时候以什么方式间隔,例如我们用 "," 分隔
def put(self, request, pk, format=None):
post = self.get_object(pk)
serializer = PostSerializer(post, data=request.data)
if serializer.is_valid():
# 记得先 clear,然后判断是否上传了 id,上传了 id 需要判断是否多个
post.tags.clear()
if request.data['tags']:
if "," in request.data['tags']:
# 我们需要提取 request.data 中 tags 所对应的值,然后通过切割字符串取出 id
for i in request.data['tags'].split(","):
post.tags.add(i)
else:
post.tags.add(request.data['tags'])
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
在 url 中还是之前的那样绑定
urlpatterns = [
url(r"^post/(?P<pk>[0-9]+)/&", views.PostDetailView.as_view(), name="api_post"),
]
修改完后我们就可以开心的更新 M2M 字段了,httpie 命令行如下
http -a [username]:[password] PUT http://192.168.x.xxx:8080/api/post/9/ ...tags=1,2...
最后附上整个项目的地址:blog_project
https://github.com/kukyxs/blog_project