Django 的類視圖
前面第9節(jié)中我們簡單介紹了 Django FBV 和 CBV,分別表示以函數(shù)形式定義的視圖和以類形式定義的視圖。函數(shù)視圖便于理解,但是如果一個視圖函數(shù)對應(yīng)的 URL 路徑支持多種不同的 HTTP 請求方式時,如 GET, POST, PUT 等,需要在一個函數(shù)中寫不同的業(yè)務(wù)邏輯,這樣導(dǎo)致寫出的函數(shù)視圖可讀性不好。此外,函數(shù)視圖的復(fù)用性不高,大量使用函數(shù)視圖,導(dǎo)致的一個結(jié)果就是大量重復(fù)邏輯和代碼,嚴(yán)重影響項目質(zhì)量。而 Django 提供的 CBV 正是要解決這個問題而出現(xiàn)的,這也是官方強(qiáng)烈推薦使用的方式。
1. Django 類視圖使用介紹
1.1 CBV 的基本使用
前面我們已經(jīng)介紹了 CBV 的基本使用方法,其基本流程如下:
定義視圖類 (TestView)
該類繼承視圖基類 View,然后實現(xiàn)對應(yīng) HTTP 請求的方法。Django 在 View 類的基礎(chǔ)上又封裝了許多視圖類,如專門返回模板的 TemplateView 視圖類、用于顯示列表數(shù)據(jù)的 ListView 視圖類等等。這些封裝的是圖能夠進(jìn)一步減少大家的重復(fù)代碼,后面我會詳細(xì)介紹這些封裝的視圖類的使用以及其源碼實現(xiàn)。
# 代碼路徑 hello_app/views.py
# ...
class TestView(View):
def get(self, request, *args, **kwargs):
return HttpResponse('hello, get\n')
def post(self, request, *args, **kwargs):
return HttpResponse('hello, post\n')
def put(self, request, *args, **kwargs):
return HttpResponse('hello, put\n')
def delete(self, request, *args, **kwargs):
return HttpResponse('hello, delete\n')
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(TestView, self).dispatch(request, *args, **kwargs)
配置 URLConf,如下:
# 代碼路徑 hello_app/urls.py
# ...
urlpatterns = [
path('test-cbv/', views.TestView.as_view(), name="test-cbv")
]
注意:不是直接寫視圖類,而是要調(diào)用視圖類的 as_view() 方法,這個 as_view() 方法返回的也是一個函數(shù)。
啟動 Django 工程,測試:
# 啟動django服務(wù)
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 15, 2020 - 07:08:32
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C
# 打開另一個xshell窗口,發(fā)送如下請求
[root@server ~]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/
hello, get
[root@server ~]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/
hello, post
[root@server ~]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/
hello, put
[root@server ~]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/
hello, delete
1.2 Django 中使用 Mixin
首先需要了解一下 Mixin 的概念,這里有一篇介紹 Python 中 Mixin 的文章:<<多重繼承>> ,可以認(rèn)真看下,加深對 Mixin 的理解。在我的理解中,Mixin 其實就是單獨的一塊功能類。假設(shè) Django 中提供了 A、B、C 三個視圖類,又有 X、Y、Z三個 Mixin 類。如果我們想要視圖 A,同時需要額外的 X、Y功能,那么使用 Python 中的多重繼承即可達(dá)到目的:
class NewView(A, X, Y):
"""
定義新的視圖
"""
pass
我們來看看 Django 的官方文檔是如何引出 Mixin 的:
Django’s built-in class-based views provide a lot of functionality, but some of it you may want to use separately. For instance, you may want to write a view that renders a template to make the HTTP response, but you can’t use TemplateView;perhaps you need to render a template only on
POST
, withGET
doing something else entirely. While you could use TemplateResponse directly, this will likely result in duplicate code.For this reason, Django also provides a number of mixins that provide more discrete functionality. Template rendering, for instance, is encapsulated in the TemplateResponseMixin.
翻譯過來就是: Django 內(nèi)置的類視圖提供了許多功能,但是我們可能只需要其中的一部分功能。例如我想寫一個視圖,該視圖使用由模板文件渲染后的 HTML 來響應(yīng)客戶端的 HTTP 請求,但是我們又不能使用 TemplateView 來實現(xiàn),因為我只想在 POST 請求上使用這個模板渲染的功能,而在 GET 請求時做其他事情。當(dāng)然,可以直接使用 TemplateResponse 來完成,這樣就會導(dǎo)致代碼重復(fù)。基于這個原因, Django 內(nèi)部提供了許多離散功能的 mixins。
可以看到,這里的 mixins 就是一些單獨功能的類,配合視圖類一起使用,用于組合出各種功能的視圖。接下來,我們結(jié)合前面的 Member 表來使用下 mixin 功能。具體的步驟如下:
改造原來的視圖類-TestView。我們給原來的視圖類多繼承一個 mixin,用于實現(xiàn)單個對象查找查找功能;
from django.shortcuts import render
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin
from .models import Member
# Create your views here.
class TestView(SingleObjectMixin, View):
model = Member
def get(self, request, *args, **kwargs):
return HttpResponse('hello, get\n')
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return HttpResponse('hello, {}\n'.format(self.object.name))
def put(self, request, *args, **kwargs):
return HttpResponse('hello, put\n')
def delete(self, request, *args, **kwargs):
return HttpResponse('hello, delete\n')
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super(TestView, self).dispatch(request, *args, **kwargs)
修改 URLConf 配置,傳遞一個動態(tài)參數(shù),用于查找表中記錄:
urlpatterns = [
path('test-cbv/<int:pk>/', views.TestView.as_view(), name="test-cbv")
]
啟動服務(wù)器,然后進(jìn)行測試:
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/2/
hello, 會員2
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/4/
hello, spyinx-0
[root@server first_django_app]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/9/
hello, spyinx-5
[root@server first_django_app]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/9/
hello, get
[root@server first_django_app]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/9/
hello, put
[root@server first_django_app]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/9/
hello, delete
可以看到在 POST 請求中,我們通過傳遞主鍵值,就能返回 Member 表中對應(yīng)記錄中的 name 字段值,這一功能正是由SingleObjectMixin
中的 get_object()
方法提供的。通過繼承這個查詢功能,我們就不用再使用 ORM 模型進(jìn)行查找了,這簡化了我們的代碼。當(dāng)然,這只能滿足一小部分的場景,對于更多復(fù)雜的場景,我們還是需要實現(xiàn)自己的邏輯,我們也可以把復(fù)雜的功能拆成各種 mixin,然后相關(guān)組合繼承,這樣可以很好的復(fù)用代碼,這是一種良好的編碼方式。
2. 深入理解 Django 類視圖
這里在介紹完類視圖的基本使用后,我們來深入學(xué)習(xí)下 Django 的源代碼,看看 Django 是如何將對應(yīng)的 HTTP 請求映射到對應(yīng)的函數(shù)上。這里我們使用的是 Django 2.2.10 的源代碼進(jìn)行說明。我們使用 VSCode 打開 Django 源碼,定位到 django/views/generic
目錄下,這里是和視圖相關(guān)的源代碼。
首先看 __init__.py
文件,內(nèi)容非常少,主要是將該目錄下的常用視圖類導(dǎo)入到這里,簡化開發(fā)者導(dǎo)入這些常用的類。其中最重要的當(dāng)屬 base.py
文件中定義的 view
類,它是其他所有視圖類的基類。
# base.py中常用的三個view類
from django.views.generic.base import RedirectView, TemplateView, View
# dates.py中定義了許多和時間相關(guān)的視圖類
from django.views.generic.dates import (
ArchiveIndexView, DateDetailView, DayArchiveView, MonthArchiveView,
TodayArchiveView, WeekArchiveView, YearArchiveView,
)
# 導(dǎo)入DetailView類
from django.views.generic.detail import DetailView
# 導(dǎo)入增刪改相關(guān)的視圖類
from django.views.generic.edit import (
CreateView, DeleteView, FormView, UpdateView,
)
# 導(dǎo)入list.py中定義的顯示列表的視圖類
from django.views.generic.list import ListView
__all__ = [
'View', 'TemplateView', 'RedirectView', 'ArchiveIndexView',
'YearArchiveView', 'MonthArchiveView', 'WeekArchiveView', 'DayArchiveView',
'TodayArchiveView', 'DateDetailView', 'DetailView', 'FormView',
'CreateView', 'UpdateView', 'DeleteView', 'ListView', 'GenericViewError',
]
# 定義一個通用的視圖異常類
class GenericViewError(Exception):
"""A problem in a generic view."""
pass
接下來,我們查看 base.py
文件,重點分析模塊中定義的 View 類:
# 源碼路徑 django/views/generic/base.py
# 忽略導(dǎo)入
# ...
class View:
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
def __init__(self, **kwargs):
# 忽略
# ...
@classonlymethod
def as_view(cls, **initkwargs):
"""Main entry point for a request-response process."""
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.setup(request, *args, **kwargs)
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
return self.dispatch(request, *args, **kwargs)
view.view_class = cls
view.view_initkwargs = initkwargs
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
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)
def http_method_not_allowed(self, request, *args, **kwargs):
logger.warning(
'Method Not Allowed (%s): %s', request.method, request.path,
extra={'status_code': 405, 'request': request}
)
return HttpResponseNotAllowed(self._allowed_methods())
# 忽略其他函數(shù)
# ...
# ...
我們來仔細(xì)分析 view 類中的這部分代碼。view 類首先定義了一個屬性 http_method_names
,表示其支持的 HTTP 請求方法。接下來最重要的是 as_view()
方法和 dispatch()
方法。在上面使用視圖類的示例中,我們定義的 URLConf 如下:
# first_django_app/hello_app/urls.py
from . import views
urlpatterns = [
# 類視圖
url(r'test-cbv/', views.TestView.as_view(), name='test-cbv'),
]
這里結(jié)合源碼可以看到,views.TestView.as_view()
返回的結(jié)果同樣是一個函數(shù):view(),它的定義和前面的視圖函數(shù)一樣。as_view()
函數(shù)可以接收一些參數(shù),函數(shù)調(diào)用會先對接收的參數(shù)進(jìn)行檢查:
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
上面的代碼會對 as_view()
函數(shù)傳遞的參數(shù)做兩方面檢查:
首先確保傳入的參數(shù)不能有 get、post 這樣的 key 值,否則會覆蓋 view 類中的對應(yīng)方法,這樣對應(yīng)的請求就無法正確找到函數(shù)進(jìn)行處理。覆蓋的代碼邏輯如下:
class View:
# ...
def __init__(self, **kwargs):
# 這里會將所有的傳入的參數(shù)通過setattr()方法給屬性類賦值
for key, value in kwargs.items():
setattr(self, key, value)
# ...
@classonlymethod
def as_view(cls, **initkwargs):
# ...
def view(request, *args, **kwargs):
# 調(diào)用視圖函數(shù)時,會將這些參數(shù)傳給View類來實例化
self = cls(**initkwargs)
# ...
# ...
# ...
此外,不可以傳遞類中不存在的屬性值。假設(shè)我們將上面的 URLConf 進(jìn)行略微修改,如下:
from . import views
urlpatterns = [
# 類視圖
url(r'test-cbv/', views.TestView.as_view(no_key='hello'), name='test-cbv'),
]
啟動后,可以發(fā)現(xiàn) Django 報錯如下,這正是由本處代碼拋出的異常。
接下來看下 update_wrapper() 方法,這個只是 python 內(nèi)置模塊中的一個方法,只是比較少用,所以會讓很多人感到陌生。先看它的作用:
update_wrapper() 這個函數(shù)的主要功能是負(fù)責(zé)復(fù)制原函數(shù)的一些屬性,如 moudle、name、doc 等。如果不加 update_wrapper(), 那么被裝飾器修飾的函數(shù)就會丟失其上面的一些屬性信息。
具體看一個測試代碼示例:
from functools import update_wrapper
def test_wrapper(f):
def wrapper_function(*args, **kwargs):
"""裝飾函數(shù),不保留原信息"""
return f(*args, **kwargs)
return wrapper_function
def test_update_wrapper(f):
def wrapper_function(*args, **kwargs):
"""裝飾函數(shù),使用update_wrapper()方法保留原信息"""
return f(*args, **kwargs)
update_wrapper(wrapper_function, f)
return wrapper_function
@test_wrapper
def test_wrapped():
"""被裝飾的函數(shù)"""
pass
@test_update_wrapper
def test_update_wrapped():
"""被裝飾的函數(shù),使用了update_wrapper()方法"""
pass
print('不使用update_wrapper()方法:')
print(test_wrapped.__doc__)
print(test_wrapped.__name__)
print()
print('使用update_wrapper()方法:')
print(test_update_wrapped.__doc__)
print(test_update_wrapped.__name__)
執(zhí)行結(jié)果如下:
不使用update_wrapper()方法:
裝飾函數(shù),不保留原信息
wrapper_function
使用update_wrapper()方法:
被裝飾的函數(shù),使用了update_wrapper()方法
test_update_wrapped
可以看到,不使用 update_wrapper() 方法的話,函數(shù)在使用裝飾器后,它的一些基本屬性比如 __name__
等都是正真執(zhí)行函數(shù)(比如上面的 wrapper_function() 函數(shù))的屬性。不過這個函數(shù)在分析視圖函數(shù)的處理流程上并不重要。接下來看 as_view 中定義的 view() 方法,它是真正執(zhí)行 HTTP 請求的視圖函數(shù):
def view(request, *args, **kwargs):
self = cls(**initkwargs)
# 如果有g(shù)et方法而沒有head方法,對于head請求則直接使用get()方法進(jìn)行處理
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
# 將Django對應(yīng)傳過來的請求實例以及相應(yīng)參數(shù)賦給實例屬性
self.setup(request, *args, **kwargs)
# 如果沒有request屬性,表明可能重寫了setup()方法,而且setup()里面忘記了調(diào)用super()
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
# 調(diào)用dispatch()方法
return self.dispatch(request, *args, **kwargs)
view() 方法里面會調(diào)用 setup()
方法將 Django 給視圖函數(shù)傳遞的參數(shù)賦給實例變量,然后會調(diào)用 dispatch()
方法去處理請求。兩個函數(shù)的代碼如下:
def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all view methods."""
self.request = request
self.args = args
self.kwargs = kwargs
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)
這里最核心的就是這個 dispatch()
方法了。首先該方法通過 request.method.lower()
這個可以拿到 http 的請求方式,比如 get、post、put 等,然后判斷是不是在預(yù)先定義好的請求方式的列表中。如果滿足,那么最核心的代碼來了:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
假設(shè)客戶端發(fā)的是 get 請求,那么 request.method.lower()
就是 “get” ,接下來執(zhí)行上面的代碼,就會得到我們定義的視圖類中定義的 get 函數(shù),最后返回的是這個函數(shù)的處理結(jié)果。這就是為啥 get 請求能對應(yīng)到視圖函數(shù)中g(shù)et() 方法的原因。其他的請求也是類似的,如果是不支持的請求,則會執(zhí)行 http_method_not_allowed()
方法。
return handler(request, *args, **kwargs)
如果對這部分代碼的執(zhí)行流程還有疑問的,我們可以在 Django 的源碼中添加幾個 print() 函數(shù),然后通過實際請求來看看執(zhí)行過程:
[root@server first_django_app]# cat ~/.pyenv/versions/django-manual/lib/python3.8/site-packages/django/views/generic/base.py
class View:
...
@classonlymethod
def as_view(cls, **initkwargs):
...
def view(request, *args, **kwargs):
print('調(diào)用view函數(shù)處理請求')
...
...
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.
print('調(diào)用dispatch()方法處理http請求,請求方式:{}'.format(request.method.lower()))
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
print('得到的handler:{}'.format(handler))
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
接下來我們還是使用前面定義的視圖類 TestView 來進(jìn)行操作,操作過程以及實驗結(jié)果如下:
# 一個窗口啟動 django 工程
(django-manual) [root@server first_django_app]# python manage.py runserver 0.0.0.0:8888
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 15, 2020 - 04:30:04
Django version 2.2.11, using settings 'first_django_app.settings'
Starting development server at http://0.0.0.0:8888/
Quit the server with CONTROL-C.
# 另一個窗口發(fā)送http請求
[root@server django-manual]# curl -XGET http://127.0.0.1:8888/hello/test-cbv/
hello, get
[root@server django-manual]# curl -XPOST http://127.0.0.1:8888/hello/test-cbv/
hello, post
[root@server django-manual]# curl -XPUT http://127.0.0.1:8888/hello/test-cbv/
hello, put
[root@server django-manual]# curl -XDELETE http://127.0.0.1:8888/hello/test-cbv/
hello, delete
3. 小結(jié)
本小節(jié)中,我們簡單介紹了視圖類的使用以及一些高級用法。接下來我們分析了 Django 源碼中的 View 類以及 Django 是如何將請求映射到對應(yīng)的函數(shù)上執(zhí)行,這部分代碼是比較簡單易懂的。只有慢慢深入了解 Django 的源代碼,了解整個 Django 框架背后為我們做的事情,才能從入門到真正掌握 Django。