Django 及DRF 源码解析

602 阅读11分钟

Django类视图

传统基于函数的视图,处理一个请求,通常如下

from django.http import HttpResponse

def register(request):
    if request == 'GET':
        pass
    if request == 'POST':
    	form = RegisterModelForm()
    	return render(request, 'register.html', {'form': form})

所有的类视图都继承django.views.generic.base.View类。

在URLconf中简单的使用通用视图

如果只是简单的做一些属性修改,可以使用as_view()方法

继承通用视图

继承是最强大和有效的使用方式,在类视图中,通过不同的类方法来处理,如下:

from django.views import View

class Test(View):

    def dispatch(self, request, *args, **kwargs):
        func = getattr(self,request.method.lower())
        ret = func(self,request,*args,**kwargs)
        return ret

    def get(self,request,*args,**kwargs):
        return HttpResponse("GET")

    def post(self,request,*args,**kwargs):
        return HttpResponse("POST")

    def put(self,request,*args,**kwargs):
        return HttpResponse("PUT")

    def delete(self,request,*args,**kwargs):
        return HttpResponse("Delete")

然后就可以像下面一样使用这个子类了。注意,由于类不是函数,所以需要使用as_view()这个类方法将一个基于类的视图转换成函数形式的接口。

#!usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'zhangbin23'
__file__ = 'urls.py'
__date__ = '2020/6/14 14:04'

from django.conf.urls import url, include
from django.contrib import admin
from register import views

app_name = '[register]'

urlpatterns = [
    url(r'^register/', views.register, name='register'),  # "app01:register"
    url(r'^test/', views.test.as_view(), name='test'),
]

这样,通过dispatch反射将会找到我们对应请求的get,post,put,delete方法,执行我们自己的方法中的业务逻辑并返回

类视图是基于反射实现根据请求方式不同,执行不同的方法

每个类视图都有一个as_view()方法,用于在urlconf中使用。这个方法会创建一个类视图的实例,并调用它的dispatch()方法。dispatch方法会在类中查找类似GET\POST之类的类方法,然后与请求request中的HTTP方法匹配。匹配上了就调用对应的代码,匹配不上就弹出异常HttpResponseNotAllowed。

as_view()源码

    @classonlymethod
    def as_view(cls, **initkwargs):  # 实际上是一个闭包  返回 view函数
        """
        Main entry point for a request-response process.
        """
        def view(request, *args, **kwargs):  # 作用:增加属性, 调用dispatch方法 
            self = cls(**initkwargs)  # 创建一个 cls 的实例对象, cls 是调用这个方法的 类(Demo)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request  # 为对象增加 request, args, kwargs 三个属性
            self.args = args
            self.kwargs = kwargs
            return self.dispatch(request, *args, **kwargs)  # 找到指定的请求方法, 并调用它

        return view

    def dispatch(self, request, *args, **kwargs):
        # Try to dispatch to the right method; if a method doesn't exist,
        if request.method.lower() in self.http_method_names:  # 判断请求的方法类视图是否拥有, http_method_names=['get', 'post']
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)  # 如果存在 取出该方法
        else:
            handler = self.http_method_not_allowed
        return handler(request, *args, **kwargs)  # 返回该请求方法执行的结果

重写或覆盖一个类属性

继承父类,在子类中重写父类的属性

class BaseView(object):
    def dispatch(self, request, *args, **kwargs):
        print('before')
        ret = super(BaseView, self).dispatch(request, *args, **kwargs)
        print('after')
        return ret

多个类共用功能,为了避免重复编写使用继承功能

DRF类视图

DRF APIView 继承View,通常如下

from rest_framework.views import APIView
import json


class RestfulTest(APIView):

    def get(self, request, *args, **kwargs):
        self.dispatch()
        ret = {'code': 1000, 'msg': 1001}
        return HttpResponse(json.dumps(ret), status=201)

Django中间件

中间件是 Django 用来处理请求和响应的钩子框架。它是一个轻量级的、底层级的“插件”系统,用于全局性地控制Django 的输入或输出,可以理解为内置的app或者小框架

django.core.handlers.base模块中定义了如何接入中间件,这也是学习Django源码的入口之一。

每个中间件组件负责实现一些特定的功能。例如,Django 包含一个中间件组件 AuthenticationMiddleware,它使用会话机制将用户与请求request关联起来。

中间件可以放在你的工程的任何地方,并以Python路径的方式进行访问。

Django 具有一些内置的中间件,并自动开启了其中的一部分,我们可以根据自己的需要进行调整。

一、如何启用中间件

若要启用中间件组件,请将其添加到 Django 配置文件settings.pyMIDDLEWARE 配置项列表中。

MIDDLEWARE 中,中间件由字符串表示。这个字符串以圆点分隔,指向中间件工厂的类或函数名的完整 Python 路径。下面是使用 django-admin startproject命令创建工程后,默认的中间件配置:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

实际上在Django中可以不使用任何中间件,如果你愿意的话,MIDDLEWARE 配置项可以为空。但是强烈建议至少使用 CommonMiddleware。而笔者的建议是保持默认的配置,这有助于你提高网站的安全性。

二、 中间件最关键的顺序问题

MIDDLEWARE 的顺序很重要,具有先后关系,因为有些中间件会依赖其他中间件。例如: AuthenticationMiddleware 需要在会话中间件中存储的经过身份验证的用户信息,因此它必须在 SessionMiddleware 后面运行 。

在请求阶段,调用视图之前,Django 按照定义的顺序执行中间件 MIDDLEWARE,自顶向下。

你可以把它想象成一个洋葱:每个中间件类都是一个“皮层”,它包裹起了洋葱的核心--实际业务视图。如果请求通过了洋葱的所有中间件层,一直到内核的视图,那么响应将在返回的过程中以相反的顺序再通过每个中间件层,最终返回给用户

如果某个层的执行过程认为当前的请求应该被拒绝,或者发生了某些错误,导致短路,直接返回了一个响应,那么剩下的中间件以及核心的视图函数都不会被执行。

三、Django内置的中间件

Django内置了下面这些中间件,满足了我们一般的需求:

Cache

缓存中间件

如果启用了该中间件,Django会以CACHE_MIDDLEWARE_SECONDS 配置的参数进行全站级别的缓存。

Common

通用中间件

该中间件为我们提供了一些便利的功能:

  • 禁止DISALLOWED_USER_AGENTS中的用户代理访问服务器
  • 自动为URL添加斜杠后缀和www前缀功能。如果配置项 APPEND_SLASHTrue ,并且访问的URL 没有斜杠后缀,在URLconf中没有匹配成功,将自动添加斜杠,然后再次匹配,如果匹配成功,就跳转到对应的url。 PREPEND_WWW 的功能类似。
  • 为非流式响应设置Content-Length头部信息。

作为展示的例子,这里额外贴出它的源代码,位于django.middleware.common模块中,比较简单,很容易读懂和理解:

class CommonMiddleware(MiddlewareMixin):
    """
    去掉了doc
    """
    response_redirect_class = HttpResponsePermanentRedirect

    def process_request(self, request):
        # Check for denied User-Agents
        if 'HTTP_USER_AGENT' in request.META:
            for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
                if user_agent_regex.search(request.META['HTTP_USER_AGENT']):
                    raise PermissionDenied('Forbidden user agent')

        # Check for a redirect based on settings.PREPEND_WWW
        host = request.get_host()
        must_prepend = settings.PREPEND_WWW and host and not host.startswith('www.')
        redirect_url = ('%s://www.%s' % (request.scheme, host)) if must_prepend else ''

        # Check if a slash should be appended
        if self.should_redirect_with_slash(request):
            path = self.get_full_path_with_slash(request)
        else:
            path = request.get_full_path()

        # Return a redirect if necessary
        if redirect_url or path != request.get_full_path():
            redirect_url += path
            return self.response_redirect_class(redirect_url)

    def should_redirect_with_slash(self, request):

        if settings.APPEND_SLASH and not request.path_info.endswith('/'):
            urlconf = getattr(request, 'urlconf', None)
            return (
                not is_valid_path(request.path_info, urlconf) and
                is_valid_path('%s/' % request.path_info, urlconf)
            )
        return False

    def get_full_path_with_slash(self, request):

        new_path = request.get_full_path(force_append_slash=True)
        if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
            raise RuntimeError(
                "You called this URL via %(method)s, but the URL doesn't end "
                "in a slash and you have APPEND_SLASH set. Django can't "
                "redirect to the slash URL while maintaining %(method)s data. "
                "Change your form to point to %(url)s (note the trailing "
                "slash), or set APPEND_SLASH=False in your Django settings." % {
                    'method': request.method,
                    'url': request.get_host() + new_path,
                }
            )
        return new_path

    def process_response(self, request, response):
        # If the given URL is "Not Found", then check if we should redirect to
        # a path with a slash appended.
        if response.status_code == 404:
            if self.should_redirect_with_slash(request):
                return self.response_redirect_class(self.get_full_path_with_slash(request))

        if settings.USE_ETAGS and self.needs_etag(response):
            warnings.warn(
                "The USE_ETAGS setting is deprecated in favor of "
                "ConditionalGetMiddleware which sets the ETag regardless of "
                "the setting. CommonMiddleware won't do ETag processing in "
                "Django 2.1.",
                RemovedInDjango21Warning
            )
            if not response.has_header('ETag'):
                set_response_etag(response)

            if response.has_header('ETag'):
                return get_conditional_response(
                    request,
                    etag=response['ETag'],
                    response=response,
                )
        # Add the Content-Length header to non-streaming responses if not
        # already set.
        if not response.streaming and not response.has_header('Content-Length'):
            response['Content-Length'] = str(len(response.content))

        return response

    def needs_etag(self, response):
        """Return True if an ETag header should be added to response."""
        cache_control_headers = cc_delim_re.split(response.get('Cache-Control', ''))
        return all(header.lower() != 'no-store' for header in cache_control_headers)

GZip

内容压缩中间件

用于减小响应体积,降低带宽压力,提高传输速度。

该中间件必须位于其它所有需要读写响应体内容的中间件之前。

如果存在下面情况之一,将不会压缩响应内容:

  • 内容少于200 bytes
  • 已经设置了 Content-Encoding 头部属性
  • 请求的 Accept-Encoding 头部属性未包含 gzip.

可以使用 gzip_page()装饰器,为视图单独开启GZip压缩服务。

Conditional GET

有条件的GET访问中间件,很少使用。

Locale

本地化中间件

用于处理国际化和本地化,语言翻译。

Message

消息中间件

基于cookie或者会话的消息功能,比较常用。

Security

安全中间件

django.middleware.security.SecurityMiddleware中间件为我们提供了一系列的网站安全保护功能。主要包括下列所示,可以单独开启或关闭:

  • SECURE_BROWSER_XSS_FILTER
  • SECURE_CONTENT_TYPE_NOSNIFF
  • SECURE_HSTS_INCLUDE_SUBDOMAINS
  • SECURE_HSTS_PRELOAD
  • SECURE_HSTS_SECONDS
  • SECURE_REDIRECT_EXEMPT
  • SECURE_SSL_HOST
  • SECURE_SSL_REDIRECT

Session

会话中间件,非常常用。

Site

站点框架。

这是一个很有用,但又被忽视的功能。

它可以让你的Django具备多站点支持的功能。

通过增加一个site属性,区分当前request请求访问的对应站点。

无需多个IP或域名,无需开启多个服务器,只需要一个site属性,就能搞定多站点服务。

Authentication

认证框架

Django最主要的中间件之一,提供用户认证服务。

CSRF protection

提供CSRF防御机制的中间件

X-Frame-Options

点击劫持防御中间件

四、自定义中间件

有时候,为了实现一些特定的需求,我们可能需要编写自己的中间件。

**在编写方式上,需要注意的是,当前Django版本2.2,存在两种编写的方式。一种是Django当前官网上提供的例子,一种是老版本的方式。**本质上,两种方式其实是一样的。

我们先看一下传统的,也是技术文章最多,目前使用最多的方式。

传统的方法

五大钩子函数

传统方式自定义中间件其实就是在编写五大钩子函数:

  • process_request(self,request)
  • process_response(self, request, response)
  • process_view(self, request, view_func, view_args, view_kwargs)
  • process_exception(self, request, exception)
  • process_template_response(self,request,response)

可以实现其中的任意一个或多个!

钩子函数执行时机执行顺序返回值
process_request请求刚到来,执行视图之前配置列表的正序None或者HttpResponse对象
process_response视图执行完毕,返回响应时逆序HttpResponse对象
process_viewprocess_request之后,路由转发到视图,执行视图之前正序None或者HttpResponse对象
process_exception视图执行中发生异常时逆序None或者HttpResponse对象
process_template_response视图刚执行完毕,process_response之前逆序实现了render方法的响应对象
process_request()

签名:process_request(request)

最主要的钩子!

只有一个参数,也就是request请求内容,和视图函数中的request是一样的。所有的中间件都是同样的request,不会发生变化。它的返回值可以是None也可以是HttpResponse对象。返回None的话,表示一切正常,继续走流程,交给下一个中间件处理。返回HttpResponse对象,则发生短路,不继续执行后面的中间件,也不执行视图函数,而将响应内容返回给浏览器。

process_response()

签名:process_response(request, response)

最主要的钩子!

有两个参数,request和response。request是请求内容,response是视图函数返回的HttpResponse对象。该方法的返回值必须是一个HttpResponse对象,不能是None。

process_response()方法在视图函数执行完毕之后执行,并且按配置顺序的逆序执行。

process_view()

签名:process_view(request, view_func, view_args, view_kwargs)

  • requestHttpRequest 对象。
  • view_func :真正的业务逻辑视图函数(不是函数的字符串名称)。
  • view_args :位置参数列表
  • view_kwargs :关键字参数字典

请务必牢记:process_view() 在Django调用真正的业务视图之前被执行,并且以正序执行。当process_request()正常执行完毕后,会进入urlconf路由阶段,并查找对应的视图,在执行视图函数之前,会先执行process_view() 中间件钩子。

这个方法必须返回None 或者一个 HttpResponse 对象。如果返回的是None,Django将继续处理当前请求,执行其它的 process_view() 中间件钩子,最后执行对应的视图。如果返回的是一个 HttpResponse 对象,Django不会调用业务视图,而是执行响应中间件,并返回结果。

process_exception()

签名:process_exception(request, exception)

  • requestHttpRequest对象
  • exception:视图函数引发的具体异常对象

当一个视图在执行过程中引发了异常,Django将自动调用中间件的 process_exception()方法。 process_exception() 要么返回一个 None ,要么返回一个 HttpResponse 对象。如果返回的是HttpResponse对象 ,模板响应和响应中间件将被调用 ,否则进行正常的异常处理流程。

同样的,此时也是以逆序的方式调用每个中间件的 process_exception方法,以短路的机制。

process_template_response()

签名:process_template_response(request, response)

requestHttpRequest 对象

responseTemplateResponse 对象

process_template_response() 方法在业务视图执行完毕后调用。

正常情况下一个视图执行完毕,会渲染一个模板,作为响应返回给用户。使用这个钩子方法,你可以重新处理渲染模板的过程,添加你需要的业务逻辑。

对于 process_template_response()方法,也是采用逆序的方式进行执行的。

钩子方法执行流程

(注:所有图片来自网络,侵删!)

一个理想状态下的中间件执行过程,可能只有process_request()process_response()方法,其流程如下:

img

一旦任何一个中间件返回了一个HttpResponse对象,立刻进入响应流程!要注意,未被执行的中间件,其响应钩子方法也不会被执行,这是一个短路,或者说剥洋葱的过程。

如果有process_view方法的介入,那么会变成下面的样子:

img

总的执行流程和机制如下图所示:

img

仔细研究一下下面的执行流程,能够加深你对中间件的理解。

img