本文是 Django 全栈开发教程的第三篇 YaDjangoBlog 之前后端分离
目录在这里,已经更新的文章如下
本文需要成四件事情:
PS: 为了打字方便,下面的:
坐稳了,开车了。
使用一个库的原因,无非就是为了:
具体落实到 DRF, 有哪些具体的优点呢?
我们先看看,不使用 DRF 的时代,API 是如何编写的。
这里我们用 function based view 来简单说明。
# 最简单版本
def simple_hello(request):
return JsonResponse({
"这就是 key": "这就是 value",
"时间": time.time()
})
刚开始学 DRF 的时候,我也有这种疑惑,这有必要需要一个 RESTFULAPI 的框架嘛?捋起袖子,JSON API 甩起来开咯。
之所以得出这个结论,是因为这个例子实在是过于简单。
当涉及到一定复杂程度的 API 的时候,问题就来了:
这都是我们需要考虑的。
如果不用 DRF, 而是由后端程序员直接写这些代码的话,也不是不行。
很显然,这是个系统性的活。 假如接下来还要考虑限流、RESTFULAPI 的设计,这就相当蛋疼了。
显然,我们的 FBV 就会是这样:
@a_authority
def complex_hello(request):
params = getParams(request)
.....
query_results = SomeModels.some_query()
.....
results = SomeModelsSerial(query_results)
.....
return JsonResponse(results)
看起来似乎是有规律可循的,既然有规律可循,就能封装一下,减轻负担。FBV 已经这样了,显然只能每次都要硬编码这些取参数,查询,序列化。当然,如果用生成器也能简化一部分函数代码。yield 实现方法太丑还是弃用吧。
我们试试 CBV 看看如何。
# 继承并重写方法
from django.views.generic import View
class APIView(View):
def get(self,request):
query_results = SomeModels.some_query()
.....
results = SomeModelsSerial(query_results)
.....
return results
def post(self,request):
query_results = SomeModels.some_query()
.....
results = SomeModelsSerial(query_results)
.....
return results
.....
# 这里相当于 view 函数
def dispatch(request, *args, **kwargs):
# 这里处理正式处理之前的逻辑,比如权限判断。
# 如果是 GET 方法,则调用
results = self.get(request, *args, **kwargs):
# 这里处理正式处理之后的逻辑,比如统计 list 的 total 值,加上时间戳
return JsonResponse(results)
于是,除了使用 FBV 进行硬编码之外,还可以使用 CBV 的基类 进行扩展定制。
我们思考一下:
恭喜你,读到这里,你已经可以写一个极简的 DRF 出来了。
但写成 DRF 这种量级的程序,还需要做很多很多事情。
要知道 DRF 的处理请求的流程,就要先知道 Django 的处理请求流程。
宏观来看
https://docs.djangoproject.com/en/2.0/topics/http/urls/
在本文的结尾的时候,我也将带大家从源码角度过一下,涉及到这个流程的相关的源码。这里先跳过。
那么,DRF 是如何处理一个请求的呢?我们忽略路由之类的东西,直接看对应的 CBV 的源码
class APIView(View):
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
# ...... 其他方法
# Dispatch methods
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
def initial(self, request, *args, **kwargs):
"""
Runs anything that needs to occur prior to calling the method handler.
"""
self.format_kwarg = self.get_format_suffix(**kwargs)
# Perform content negotiation and store the accepted info on the request
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
# Determine the API version, if versioning is in use.
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)
# Note: Views are made CSRF exempt from within `as_view` as to prevent
# accidental removal of this exemption in cases where `dispatch` needs to
# be overridden.
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
# 这里需要注意
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
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
可以看出,当请求到达 dispatch 的时候,DRF 添加了一些钩子函数,用于开始 / 结束 / 错误控制。
一看,其实与我们之前想封装 APIView 的想法不谋而合,而我们只是想想,DRF 是详细实现。
回到我们的 yadjangoblog 上面来。这个时候我们想开一个博文列表 API:
# 1. 定义序列器,用于序列化查询的每一条。
class BlogPostListSerializer(serializers.ModelSerializer):
category = BlogCategorySerializer(read_only=True)
tags = BlogTagSerializer(many=True, read_only=True)
title = serializers.CharField()
id = serializers.IntegerField()
class Meta:
model = BlogPost
fields = ('id', 'title', 'char_num', 'vote_num', 'category', 'tags', 'publish_date')
# 2. 定义过滤器,可以通过过滤器进行查询
class BlogPostFilter(filters.FilterSet):
title = filters.CharFilter(lookup_expr='contains')
having_tags = filters.Filter(name="tags", lookup_expr='in')
class Meta:
model = BlogPost
fields = ('title', 'char_num', 'category', 'tags')
# 3. 指定其他设置,具体大家看源码就好了。
class BlogPostListAPIView(generics.ListAPIView):
"""
依照 category , tags , 时间 (年 / 月 / 日 年 / 月 年)
"""
queryset = BlogPost.objects.all()
serializer_class = BlogPostListSerializer
filter_backends = (filters.DjangoFilterBackend, OrderingFilter,)
filter_class = BlogPostFilter
ordering_fields = ('publish_date',)
ordering = ('publish_date',)
permission_classes = (permissions.AllowAny,)
pagination_class = SmallResultsSetPagination
在指定上面的操作之后,一个接口就快速的开出来了。
: TODO 插入一张图
当然,DRF 认认真真通读一遍的话,还是可以给自己节省不少时间的。
这是开接口,似乎,还少了什么,比如 Restful API.
什么是 CORS 可以参考阮一峰的文章 http://www.ruanyifeng.com/blog/2016/04/cors.html
在调试的时候,我们肯定是使用 ajax / fetch 方式请求。这就会遇到一个问题:
解决方式也很简单,服务端只要服务器实现了 CORS 接口,就可以跨源通信。
安装 django-cors-headers, 并在 settings 中开启 CORS_ORIGIN_ALLOW_ALL = True 即可。
这里参考了临书的解决方案,要感谢 @临书 , 附上参考地址 https://zhuanlan.zhihu.com/p/24893786
对于本项目而言,使用了 axios 请求库,直接 get 即可。详细看前端代码即可。
开发过程中,尽量靠近 RESTFUL API 的设计,而不是照搬。
举个其他领域的例子,有的人表述美就只有:
但是不同的美各有各的模样:
同样,放在 RESFUL 的时候确实也出现了这种情况:
几乎所有的业务逻辑最后会落实到数据表的 CURDE, 但是所有业务逻辑并不能完全使用 CRUDE 描述。
我们看下面的例子
举个例子,RESTFUL 适合纯粹 CURDE 的设计风格。
比如,新增博客,更新博客,查询博客,删除博客,查看是否含有博客
但语义在某些场景下表述不足, 比如,设计订单的时候,
URL: /api/v1/user/some_user/orders
你查看订单集合,这个好理解。get 方法
你新增订单,这个好理解。put 方法
URL: /api/v1/user/some_user/order/xxxxxxx
你删除订单,这个好理解。delete 方法
你获取订单,这个好理解。get 方法
你修改订单,这个好理解。post 方法
但修改订单,有的时候可能会比较复杂,有可能是取消订单,有可能是评价订单,有可能是其他。而 RESTFUL 表达这种情况就有些语义不足了。
当然,个人经验是,字段越多,越难靠近 RESTFUL 规范
这个时候,就需要设计者做好 RESTFULAPI 的设计与语义化的平衡了。
关于响应设计,主要有两点需要注意:
HTTP 状态码用于标记资源情况,比如:
200 表示获取资源
404 表示 NOT FOUND
但有时候也存在语义表达不足问题,一般前后端也会约定一个通用的状态码
通用状态码 错误信息 含义 HTTP 状态码
999 unknow_v2_error 未知错误 400
1000 need_permission 需要权限 403
1001 uri_not_found 资源不存在 404
1002 missing_args 参数不全 400
1003 image_too_large 上传的图片太大 400
....
至于响应内容,一般都是见招拆招的。建议查看文章末尾的 Douban 的相关 API 规范来提升姿势。
这小节属于一时兴起写的番外篇。和本文主体内容没啥必要的关联。不感兴趣的可以直接跳转到文章末尾点赞哈。
WSGI 全称叫做 web 服务器网关接口,通常情况下,gunicorn 或者 uwsgi 接收来自 nginx 转发来的请求之后,向 web app 提供了环境信息(叫请求上下文会不会好些)以及一个 callback. 这样的话,web app 就可以接收这个环境信息,处理完毕,通过回调函数处理请求,并返回响应。一个极简的 webapp 如下:
def app(environ, start_response):
"""Simplest possible application object"""
data = 'Hello, World!\n'
status = '200 OK'
response_headers = [
('Content-type','text/plain'),
('Content-Length', str(len(data)))
]
start_response(status, response_headers)
return iter([data])
现在我们看看 django 中是如何处理请求的。首先查看相关的 wsgi.py
# wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()
# 接着查看 get_wsgi_application
import django
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
"""
The public interface to Django's WSGI support. Return a WSGI callable.
Avoids making django.core.handlers.WSGIHandler a public API, in case the
internal WSGI implementation changes or moves in the future.
"""
django.setup(set_prefix=False)
return WSGIHandler()
# 于是自然而言的看到了 WSGIHandler
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_middleware()
def __call__(self, environ, start_response):
# 有木有看到 environ 和 start_response ?? 这就是极简 web app 中的 webapp 核心方法。
set_script_prefix(get_script_name(environ))
signals.request_started.send(sender=self.__class__, environ=environ)
request = self.request_class(environ)
# 注意这一行,有请求处理逻辑 具体要见下面代码
response = self.get_response(request)
# ......
return response
嗯,看到了子类,就要看看基类
class BaseHandler:
_request_middleware = None
_view_middleware = None
_template_response_middleware = None
_response_middleware = None
_exception_middleware = None
_middleware_chain = None
def load_middleware(self):
"""
注册 MiddleWare, 并赋值 _middleware_chain 方法,使之调用的时候可以先按照顺序从 setting 的 middleware 里面处理 requests
并在处理 request 的最后调用 私有方法 _get_response
"""
self._request_middleware = []
self._view_middleware = []
self._template_response_middleware = []
self._response_middleware = []
self._exception_middleware = []
handler = convert_exception_to_response(self._get_response)
# 注意,这里面是倒着来的 代码中越在前面,实际运行的时候处理就越在后面
for middleware_path in reversed(settings.MIDDLEWARE):
# 依次添加 view middleware / template middleware / exception middleware
middleware = import_string(middleware_path)
mw_instance = middleware(handler)
handler = convert_exception_to_response(mw_instance)
# We only assign to this when initialization is complete as it is used
# as a flag for initialization being complete.
self._middleware_chain = handler
.....
def get_response(self, request):
"""Return an HttpResponse object for the given HttpRequest."""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
# ......
return response
def _get_response(self, request):
"""
Resolve and call the view, then apply view, exception, and
template_response middleware. This method is everything that happens
inside the request/response middleware.
"""
response = None
# 1. 接着判断 urlconf (默认为 ROOT_URLCONF), 可以通过 middleware 进行设置
if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()
resolver_match = resolver.resolve(request.path_info)
callback, callback_args, callback_kwargs = resolver_match
request.resolver_match = resolver_match
# Apply view middleware....
# 注意,这个就是 view 函数
wrapped_callback = self.make_view_atomic(callback)
response = wrapped_callback(request, *callback_args, **callback_kwargs)
# Complain if the view returned None (a common error).
return response
def process_exception_by_middleware(self, exception, request):
# ......
上面代码比较表达的意思比较简单,值得注意的地方我都加了注释。
需要特别注意的就是 middleware_chain 这个属性(实际上是一个方法), 正是这个方法使得注册的 middleware (在 load_middleware 方法里)可以在 fbv 或者 cbv 处理 request 之前,通过对 request 进行处理。
还犹豫啥,Django 前后端分离最佳实践,点赞后,快上车吧
ChangeLog: