1. 程式人生 > >Django - 類檢視與中介軟體

Django - 類檢視與中介軟體

1 類檢視

1.1 類檢視引入

以函式的方式定義的檢視稱為函式檢視,函式檢視便於理解。但是遇到一個檢視對應的路徑提供了多種不同HTTP請求方式的支援時,便需要在一個函式中編寫不同的業務邏輯,程式碼可讀性與複用性都不佳。

 def register(request):
    """處理註冊"""

    # 獲取請求方法,判斷是GET/POST請求
    if request.method == 'GET':
        # 處理GET請求,返回註冊頁面
        return render(request, 'register.html')
    else:
        # 處理POST請求,實現註冊邏輯
        return HttpResponse('這裡實現註冊邏輯')

在Django中也可以使用類來定義一個檢視,稱為類檢視

使用類檢視可以將檢視對應的不同請求方式以類中的不同方法來區別定義。如下所示

from django.views.generic import View

class RegisterView(View):
    """類檢視:處理註冊"""

    def get(self, request):
        """處理GET請求,返回註冊頁面"""
        return render(request, 'register.html')

    def post(self, request):
        """處理POST請求,實現註冊邏輯"""
        return HttpResponse('這裡實現註冊邏輯')

類檢視的好處:

  • 程式碼可讀性好
  • 類檢視相對於函式檢視有更高的複用性, 如果其他地方需要用到某個類檢視的某個特定邏輯,直接繼承該類檢視即可

1.2 類檢視使用

定義類檢視需要繼承自Django提供的父類View,可使用from django.views.generic import View或者from django.views.generic.base import View 匯入,定義方式如上所示。

配置路由時,使用類檢視的as_view()方法來新增

urlpatterns = [
    # 檢視函式:註冊
    # url(r'^register/$', views.register, name='register'),
    # 類檢視:註冊
    url(r'^register/$', views.RegisterView.as_view(), name='register'),
]

1.3 類檢視原理

    @classonlymethod
    def as_view(cls, **initkwargs):
        """
        Main entry point for a request-response process.
        """
        ...省略程式碼...

        def view(request, *args, **kwargs):
            self = cls(**initkwargs)
            if hasattr(self, 'get') and not hasattr(self, 'head'):
                self.head = self.get
            self.request = request
            self.args = args
            self.kwargs = kwargs
            # 呼叫dispatch方法,按照不同請求方式呼叫不同請求方法
            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,
        # defer to the error handler. Also defer to the error handler if the
        # request method isn't on the approved list.
        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
        return handler(request, *args, **kwargs)

1.4 類檢視使用裝飾器

為類檢視新增裝飾器,可以使用三種方法。

為了理解方便,我們先來定義一個為函式檢視準備的裝飾器(在設計裝飾器時基本都以函式檢視作為考慮的被裝飾物件),及一個要被裝飾的類檢視。

def my_decorator(func):
    def wrapper(request, *args, **kwargs):
        print('自定義裝飾器被呼叫了')
        print('請求路徑%s' % request.path)
        return func(request, *args, **kwargs)
    return wrapper

class DemoView(View):
    def get(self, request):
        print('get方法')
        return HttpResponse('ok')

    def post(self, request):
        print('post方法')
        return HttpResponse('ok')

1.4.1 在URL配置中裝飾

urlpatterns = [
    url(r'^demo/$', my_decorate(DemoView.as_view()))
]

此種方式最簡單,但因裝飾行為被放置到了url配置中,單看檢視的時候無法知道此檢視還被添加了裝飾器,不利於程式碼的完整性,不建議使用。

此種方式會為類檢視中的所有請求方法都加上裝飾器行為(因為是在檢視入口處,分發請求方式前)。

1.4.2 在類檢視中裝飾

在類檢視中使用為函式檢視準備的裝飾器時,不能直接新增裝飾器,需要使用method_decorator將其轉換為適用於類檢視方法的裝飾器。

from django.utils.decorators import method_decorator

# 為全部請求方法新增裝飾器
class DemoView(View):

    @method_decorator(my_decorator)
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)

    def get(self, request):
        print('get方法')
        return HttpResponse('ok')

    def post(self, request):
        print('post方法')
        return HttpResponse('ok')


# 為特定請求方法新增裝飾器
class DemoView(View):

    @method_decorator(my_decorator)
    def get(self, request):
        print('get方法')
        return HttpResponse('ok')

    def post(self, request):
        print('post方法')
        return HttpResponse('ok')

method_decorator裝飾器還支援使用name引數指明被裝飾的方法

# 為全部請求方法新增裝飾器
@method_decorator(my_decorator, name='dispatch')
class DemoView(View):
    def get(self, request):
        print('get方法')
        return HttpResponse('ok')

    def post(self, request):
        print('post方法')
        return HttpResponse('ok')


# 為特定請求方法新增裝飾器
@method_decorator(my_decorator, name='get')
class DemoView(View):
    def get(self, request):
        print('get方法')
        return HttpResponse('ok')

    def post(self, request):
        print('post方法')
        return HttpResponse('ok')

為什麼需要使用method_decorator???

為函式檢視準備的裝飾器,其被呼叫時,第一個引數用於接收request物件

def my_decorate(func):
    def wrapper(request, *args, **kwargs):  # 第一個引數request物件
        ...程式碼省略...
        return func(request, *args, **kwargs)
    return wrapper

而類檢視中請求方法被呼叫時,傳入的第一個引數不是request物件,而是self 檢視物件本身,第二個位置引數才是request物件

class DemoView(View):
    def dispatch(self, request, *args, **kwargs):
        ...程式碼省略...

    def get(self, request):
        ...程式碼省略...

所以如果直接將用於函式檢視的裝飾器裝飾類檢視方法,會導致引數傳遞出現問題。

method_decorator的作用是為函式檢視裝飾器補充第一個self引數,以適配類檢視方法。

如果將裝飾器本身改為可以適配類檢視方法的,類似如下,則無需再使用method_decorator。

def my_decorator(func):
    def wrapper(self, request, *args, **kwargs):  # 此處增加了self
        print('自定義裝飾器被呼叫了')
        print('請求路徑%s' % request.path)
        return func(self, request, *args, **kwargs)  # 此處增加了self
    return wrapper

1.4.3 構造Mixin擴充套件類

之前重寫過dispatch,然後給dispatch新增裝飾器,也考慮瞭如果可以給as_view的返回值view新增裝飾器也是可以的。

使用面向物件多繼承的特性。

class MyDecoratorMixin(View):
    @classmethod
    def as_view(cls, *args, **kwargs):
        view = super().as_view(*args, **kwargs)
        view = my_decorator(view)
        return view

class DemoView(MyDecoratorMixin):
    def get(self, request):
        print('get方法')
        return HttpResponse('ok')

    def post(self, request):
        print('post方法')
        return HttpResponse('ok')

使用Mixin擴充套件類,也會為類檢視的所有請求方法都新增裝飾行為。

  • 如果想同時對兩個類檢視,新增裝飾器:
class DemoView1(View):
    def get(self, request):
        return HttpResponse("get page")

    def post(self, request):
        return HttpResponse("post page")

class DemoView2(View):
    def get(self, request):
        return HttpResponse("get page")

    def post(self, request):
        return HttpResponse("post page")

那麼同時讓DemoView1和DemoView2都重寫as_view,這個邏輯都一樣,所以我們來抽取一個基類:BaseView

程式碼如下:

class BaseView(View):
    @classmethod
    def as_view(cls, *args, **kwargs):
        view = super().as_view(*args, **kwargs)
        view = my_decorator(view)
        return view

讓DemoView1和DemoView2都繼承BaseView:

class DemoView1(BaseView):
    def get(self, request):
        return HttpResponse("get page")

    def post(self, request):
        return HttpResponse("post page")

class DemoView2(BaseView):
    def get(self, request):
        return HttpResponse("get page")

    def post(self, request):
        return HttpResponse("post page")

來做個分析:

  • 如果想一個類檢視同時繼承多個擴充套件類

剛剛我們是要對兩個類檢視新增裝飾器,接下來我們再來看一個問題,要對一個類檢視新增兩個不同的裝飾器:

那按照剛剛的邏輯,我們就需要有第二個繼承View的擴充套件類:比如Base2View

class BaseView(View):
    @classmethod
    def as_view(cls, *args, **kwargs):
        view = super().as_view(*args, **kwargs)
        view = my_decorator(view)
        return view


class Base2View(View):
    @classmethod
    def as_view(cls, *args, **kwargs):
        view = super().as_view(*args, **kwargs)
        view = my_decorator(view)
        return view


class DemoView(BaseView, BaseView2):
    def get(self, request):
        return HttpResponse("get page")

    def post(self, request):
        return HttpResponse("post page")

然後DemoView同時繼承兩個父類,如果這樣做其實也是ok的。

分析如下:

其實剛剛的多個擴充套件類是有問題的,以為那種情況是有限制的,必須兩個擴充套件類都繼承View才可以。

如果有一個不繼承View,倆擴充套件類的父類不一樣,就會出問題。

如下:

上圖的呼叫邏輯:

  1. 呼叫DemoView的as_view()
  2. 呼叫BaseView的as_view()
  3. 呼叫View的as_view()

到這就完了,不會呼叫Base2View的as_view,這樣就少了一個裝飾器裝飾。

如何解決呢?

其實我們讓BaseView和Base2View保證是一個父類即可,都繼承object

class BaseView(object):
    @classmethod
    def as_view(cls, *args, **kwargs):
        view = super().as_view(*args, **kwargs)
        view = my_decorator(view)
        return view


class Base2View(object):
    @classmethod
    def as_view(cls, *args, **kwargs):
        view = super().as_view(*args, **kwargs)
        view = my_decorator(view)
        return view


class DemoView1(BaseView, BaseView2, View):
    def get(self, request):
        return HttpResponse("get page")

    def post(self, request):
        return HttpResponse("post page")

2 中介軟體

Django中的中介軟體是一個輕量級、底層的外掛系統,可以介入Django的請求和響應處理過程,修改Django的輸入或輸出。中介軟體的設計為開發者提供了一種無侵入式的開發方式,增強了Django框架的健壯性。

我們可以使用中介軟體,在Django處理檢視的不同階段對輸入或輸出進行干預。

2.1 中介軟體的定義方法

定義一箇中間件工廠函式,然後返回一個可以被呼叫的中介軟體。

中介軟體工廠函式需要接收一個可以呼叫的get_response物件。

返回的中介軟體也是一個可以被呼叫的物件,並且像檢視一樣需要接收一個request物件引數,返回一個response物件。

def simple_middleware(get_response):
    # 此處編寫的程式碼僅在Django第一次配置和初始化的時候執行一次。

    def middleware(request):
        # 此處編寫的程式碼會在每個請求處理檢視前被呼叫。

        response = get_response(request)

        # 此處編寫的程式碼會在每個請求處理檢視之後被呼叫。

        return response

    return middleware

例如,在users應用中新建一個middleware.py檔案,

def my_middleware(get_response):
    print('init 被呼叫')
    def middleware(request):
        print('before request 被呼叫')
        response = get_response(request)
        print('after response 被呼叫')
        return response
    return middleware

定義好中介軟體後,需要在settings.py 檔案中添加註冊中介軟體

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',
    "users.middleware.my_middleware",  # 新增中介軟體
]

定義一個檢視進行測試

def demo_view(request):
    print('view 檢視被呼叫')
    return HttpResponse('OK')

執行結果

注意:Django執行在除錯模式下,中介軟體init部分有可能被呼叫兩次。

總結:

  1. init只會在啟動專案的時候,才會呼叫。
  2. 而before request 和 after response 在每次檢視函式執行的時候都會執行。

2.2 多箇中間件的執行順序

  • 初始化,中介軟體由下至上依次執行
  • 在請求檢視被處理,中介軟體由上至下依次執行
  • 在請求檢視被處理,中介軟體由下至上依次執行

示例:

定義兩個中介軟體

def my_middleware(get_response):
    print('init 被呼叫')
    def middleware(request):
        print('before request 被呼叫')
        response = get_response(request)
        print('after response 被呼叫')
        return response
    return middleware

def my_middleware2(get_response):
    print('init2 被呼叫')
    def middleware(request):
        print('before request 2 被呼叫')
        response = get_response(request)
        print('after response 2 被呼叫')
        return response
    return middleware

註冊新增兩個中介軟體

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',
    'users.middleware.my_middleware',  # 新增
    'users.middleware.my_middleware2',  # 新增
]

執行結果

init2 被呼叫
init 被呼叫
before request 被呼叫
before request 2 被呼叫
view 檢視被呼叫
after response 2 被呼叫
after response 被呼叫