Django 全栈开发教程之03 - YaDjangoBlog 之前后端分离篇

2,521 阅读12分钟
原文链接: zhuanlan.zhihu.com

0x00 前言

本文是 Django 全栈开发教程的第三篇 YaDjangoBlog 之前后端分离

目录在这里,已经更新的文章如下

本文需要成四件事情:

  • 第一件事情,解读 DjangoRestFramework, 通过简单的例子来引入用 DRF 的必要性,并且简单介绍 DRF 的 CBV 实现。
  • 第二件事情,简单介绍 DRF 在本项目 YaDjangoBlog 中的使用
  • 第三件事情,简单聊聊 RESTFULAPI 规范,并给出最佳实践参考。
  • 第四件事情,简单解读一下 Django 处理请求流程代码。

PS: 为了打字方便,下面的:

  • DRF 指的是 DjangoRestFramework
  • CBV 指的是 Class Based View
  • FBV 指的是 Function Based View
坐稳了,开车了。

0x01 DjangorestFramework 解读

为什么要用 DRF 呢?

使用一个库的原因,无非就是为了:

  1. 节省开发者自己造轮子的时间。
  2. 有利于代码的可维护性 / 或者程序的健壮性。

具体落实到 DRF, 有哪些具体的优点呢?

  1. 可直接浏览调试的界面。让前端调试起来欲罢不能的功能。
  2. 用 DRF 的方式快速批量开接口
  3. 分页、序列化、校验、登录、权限、Web 附加文档、限流,高度的可扩展性。哪里不爽扩展哪里,so easy
  4. 算的上是 Django 社区最好的 RESTFUL 框架的轮子了。
  5. 完善的社区支持,比如 guardian/django-filter 等等结合。

不使用 DRF 应该如何写 WebAPI 做呢?

我们先看看,不使用 DRF 的时代,API 是如何编写的。

这里我们用 function based view 来简单说明。

# 最简单版本
def simple_hello(request):
    return JsonResponse({
        "这就是 key": "这就是 value",
        "时间": time.time()
    })

刚开始学 DRF 的时候,我也有这种疑惑,这有必要需要一个 RESTFULAPI 的框架嘛?捋起袖子,JSON API 甩起来开咯。

之所以得出这个结论,是因为这个例子实在是过于简单。

当涉及到一定复杂程度的 API 的时候,问题就来了:

  1. 权限是否需要区分?
  2. 分页需不需要做?
  3. 前端人员提交 Form 表单时,只能通过命令行或者是 POSTMAN 之类的工具提交参数,这会不会带来不便?后端人员写这些表单的各个字段,也是很手酸的事情。
  4. 拼接字典或者是字符串倒也还好,能不能有个序列器帮我直接序列化这模型,并且如果模型和模型之间有联系,最好也可以帮我完成模型和模型之间的关联。
  5. Profile API 应该如何做?

这都是我们需要考虑的。

如果不用 DRF, 而是由后端程序员直接写这些代码的话,也不是不行。

  1. 对于第一点,可以直接在 fbv 上面加装饰器。
  2. 对于第二点,分页的时候可以直接将逻辑写在 fbv 里面。
  3. 前端 er 直接使用 PostMan 之类的工具就好了。
  4. 序列化,可以借助内置的序列化方法。
  5. Profile 可以在提交参数的时候,附加一个参数比如 debug, 渲染的时候,将使用 HTML 里面内置一个 JSON 字符串的方式渲染出来。这样的话,就可以使用 Django Debug Tools 进行 Profile 了。

很显然,这是个系统性的活。 假如接下来还要考虑限流、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 的基类 进行扩展定制。

我们思考一下:

  1. 假如我想渲染某个模型的 JSON 列表,就可以定制一个 ListViewAPI 出来。如果需要一个 DetailViewAPI, 就定制一个 DetailViewAPI 出来。
  2. 我们再声明一些 Permission 类,序列化类,模型,然后在 dispatch 中直接使用这些东西的话,就只需要在 get 和 post 里面编写一些最核心的逻辑了。
  3. 甚至,指定了分页器和查询,都完全不需要再 get 和 post 里面写代码。

恭喜你,读到这里,你已经可以写一个极简的 DRF 出来了。

但写成 DRF 这种量级的程序,还需要做很多很多事情。

DRF 处理请求的流程

要知道 DRF 的处理请求的流程,就要先知道 Django 的处理请求流程。

宏观来看

  1. 请求先经过 MiddleWare , 接着判断 urlconf (默认为 ROOT_URLCONF),
  2. 匹配 URL, 将请求上下文 dispatch 到具体的 view.
  3. 处理完毕,经过 MiddleWare

docs.djangoproject.com/en/2.0/topi…

在本文的结尾的时候,我也将带大家从源码角度过一下,涉及到这个流程的相关的源码。这里先跳过。

那么,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 添加了一些钩子函数,用于开始 / 结束 / 错误控制。

  1. 在 initialize_request 的时候,对 request 进行封装,添加上 parser / auth / negoriator / parser context
  2. 接着在 initial 方法里面校验了版本,进行了认证和鉴权,检查了限流

一看,其实与我们之前想封装 APIView 的想法不谋而合,而我们只是想想,DRF 是详细实现。

0x02 DjangorestFramework 的使用案例

如何开 WebAPI 接口

回到我们的 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

在指定上面的操作之后,一个接口就快速的开出来了。

当然,DRF 认认真真通读一遍的话,还是可以给自己节省不少时间的。

这是开接口,前端应该如何使用接口呢

前端如何使用 WebAPI 接口

什么是 CORS 可以参考阮一峰的文章 www.ruanyifeng.com/blog/2016/0…

在调试的时候,我们肯定是使用 ajax / fetch 方式请求。这就会遇到一个问题:

  • 跨域

解决方式也很简单,服务端只要服务器实现了 CORS 接口,就可以跨源通信。

安装 django-cors-headers, 并在 settings 中开启 CORS_ORIGIN_ALLOW_ALL = True 即可。

这里参考了临书的解决方案,要感谢 @临书 , 附上参考地址 zhuanlan.zhihu.com/p/24893786

对于本项目而言,使用了 axios 请求库,直接 get 即可。详细看前端代码即可。

0x03 RESTFUL API 设计

开发过程中,尽量靠近 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 状态码,也业务逻辑通用状态码)
  • 响应内容 包含 业务逻辑通用状态码,剩下的视具体情况而定。

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 规范来提升姿势。

0x04 Django 的处理请求流程代码解读

这小节属于一时兴起写的番外篇。和本文主体内容没啥必要的关联。不感兴趣的可以直接跳转到文章末尾点赞哈。

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 进行处理。

0xEE. 参考链接

还犹豫啥,Django 前后端分离最佳实践,点赞后,快上车吧


ChangeLog:

  • 2018-02-22 开启本文
  • 2018-03-04 重修文字