1. 程式人生 > 程式設計 >Django 官方推薦的姿勢:類檢視

Django 官方推薦的姿勢:類檢視

作者:HelloGitHub-追夢人物

文中所涉及的示例程式碼,已同步更新到 HelloGitHub-Team 倉庫

在開發網站的過程中,有一些檢視函式雖然處理的物件不同,但是其大致的程式碼邏輯是一樣的。比如一個部落格和一個論壇,通常其首頁都是展示一系列的文章列表或者帖子列表。對處理首頁的檢視函式來說,雖然其處理的物件一個是文章,另一個是帖子,但是其處理的過程是非常類似的:首先是從資料庫取出文章或者帖子列表,然後將這些資料傳遞給模板並渲染模板。於是,django 把這些相同的邏輯程式碼抽取了出來,寫成了一系列的通用檢視函式,即基於類的通用檢視(Generic Class Based View)。

使用類檢視是 django 推薦的做法,熟悉了類檢視的使用方法後,能夠減少檢視函式的重複程式碼,節省開發時間。接下來就讓我們把部落格應用中的檢視函式改成基於類的通用檢視。

ListView

在我們的部落格應用中,有幾個檢視函式是從資料庫中獲取文章(Post)列表資料的:

blog/views.py

def index(request):
    # ...
    
def archive(request,year,month):
    # ...
    
def category(request,pk):
    # ...
    
def tag(request,pk):
    # ...
複製程式碼

這些檢視函式都是從資料庫中獲取文章(Post)列表,唯一的區別就是獲取的文章列表可能不同。比如 index 獲取全部文章列表,category 獲取某個分類下的文章列表。

將 index 檢視函式改寫為類檢視

針對這種從資料庫中獲取某個模型列表資料(比如這裡的 Post 列表)的檢視,Django 專門提供了一個 ListView 類檢視。下面我們通過一個例子來看看 ListView 的使用方法。我們首先把 index 檢視函式改造成類檢視函式。

blog/views.py

from django.views.generic import ListView

class IndexView
(ListView):
model = Post template_name = 'blog/index.html' context_object_name = 'post_list' 複製程式碼

要寫一個類檢視,首先需要繼承 django 提供的某個類檢視。至於繼承哪個類檢視,需要根據你的檢視功能而定。比如這裡 IndexView 的功能是從資料庫中獲取文章(Post)列表,ListView 就是從資料庫中獲取某個模型列表資料的,所以 IndexView 繼承 ListView

然後就是通過一些屬性來指定這個檢視函式需要做的事情,這裡我們指定了三個屬性:

  • model:將 model 指定為 Post,告訴 django 我要獲取的模型是 Post
  • template_name:指定這個檢視渲染的模板。
  • context_object_name:指定獲取的模型列表資料儲存的變數名,這個變數會被傳遞給模板。

如果還是有點難以理解,不妨將類檢視的程式碼和 index 檢視函式的程式碼對比一下:

blog/views.py

def index(request):
    post_list = Post.objects.all()
    return render(request,'blog/index.html',context={'post_list': post_list})
複製程式碼

index 檢視函式首先通過 Post.objects.all() 從資料庫中獲取文章(Post)列表資料,並將其儲存到 post_list 變數中。而在類檢視中這個過程 ListView 已經幫我們做了。我們只需告訴 ListView 去資料庫獲取的模型是 Post,而不是 Comment 或者其它什麼模型,即指定 model = Post。將獲得的模型資料列表儲存到 post_list 裡,即指定 context_object_name = 'post_list'。然後渲染 blog/index.html 模板檔案,index 檢視函式中使用 render 函式。但這個過程 ListView 已經幫我們做了,我們只需指定渲染哪個模板即可。

接下來就是要將類檢視轉換成函式檢視。為什麼需要將類檢視轉換成函式檢視呢?

我們來看一看 blog 的 URL 配置:

blog/urls.py

app_name = 'blog'
urlpatterns = [
    path('',views.index,name='index'),...
]
複製程式碼

前面已經說過每一個 URL 對應著一個檢視函式,這樣當使用者訪問這個 URL 時,Django 就知道呼叫哪個檢視函式去處理這個請求了。在 Django 中 URL 模式的配置方式就是通過 url 函式將 URL 和檢視函式繫結。比如 path('',name='index'),它的第一個引數是 URL 模式,第二個引數是檢視函式 index。對 url 函式來說,第二個引數傳入的值必須是一個函式。而 IndexView 是一個類,不能直接替代 index 函式。好在將類檢視轉換成函式檢視非常簡單,只需呼叫類檢視的 as_view() 方法即可(至於 as_view 方法究竟是如何將一個類轉換成一個函式的目前不必關心,只需要在配置 URL 模式是呼叫 as_view 方法就可以了。具體的實現我們以後會專門開闢一個專欄分析類檢視的原始碼,到時候就能看出 django 使用的魔法了)。

現在在 URL 配置中把 index 檢視替換成類檢視 IndexView

blog/urls.py

app_name = 'blog'
urlpatterns = [
    path('',views.IndexView.as_view(),...
]
複製程式碼

訪問一下首頁,可以看到首頁依然顯示全部文章列表,和使用檢視函式 index 時效果一模一樣。

將 category 檢視函式改寫為類檢視

category 檢視函式的功能也是從資料庫中獲取文章列表資料,不過其和 index 檢視函式不同的是,它獲取的是某個分類下的全部文章。因此 category 檢視函式中多了一步,即首先需要根據從 URL 中捕獲的分類 id 並從資料庫獲取分類,然後使用 filter 函式過濾出該分類下的全部文章。來看看這種情況下類檢視該怎麼寫:

blog/views.py

class CategoryView(ListView):
    model = Post
    template_name = 'blog/index.html'
    context_object_name = 'post_list'

    def get_queryset(self):
        cate = get_object_or_404(Category,pk=self.kwargs.get('pk'))
        return super(CategoryView,self).get_queryset().filter(category=cate)
複製程式碼

IndexView 不同的地方是,我們覆寫了父類的 get_queryset 方法。該方法預設獲取指定模型的全部列表資料。為了獲取指定分類下的文章列表資料,我們覆寫該方法,改變它的預設行為。

首先是需要根據從 URL 中捕獲的分類 id(也就是 pk)獲取分類,這和 category 檢視函式中的過程是一樣的。不過注意一點的是,在類檢視中,從 URL 捕獲的路徑引數值儲存在例項的 kwargs 屬性(是一個字典)裡,非路徑引數值儲存在例項的 args 屬性(是一個列表)裡。所以我們使了 self.kwargs.get('pk') 來獲取從 URL 捕獲的分類 id 值。然後我們呼叫父類的 get_queryset 方法獲得全部文章列表,緊接著就對返回的結果呼叫了 filter 方法來篩選該分類下的全部文章並返回。

此外我們可以看到 CategoryView 類中指定的屬性值和 IndexView 中是一模一樣的,所以如果為了進一步節省程式碼,甚至可以直接繼承 IndexView

class CategoryView(IndexView):
    def get_queryset(self):
        cate = get_object_or_404(Category,self).get_queryset().filter(category=cate)
複製程式碼

然後就在 URL 配置中把 category 檢視替換成類檢視 CategoryView

blog/urls.py

app_name = 'blog'
urlpatterns = [
    ...
    path('categories/<int:pk>/',views.CategoryView.as_view(),name='category'),]
複製程式碼

訪問以下某個分類頁面,可以看到依然顯示的是該分類下的全部文章列表,和使用檢視函式 category 時效果一模一樣。

將 archive 和 tag 檢視函式改寫成類檢視

這裡沒有什麼新東西要講了,學以致用,這個任務就交給你自己了。

DetailView

除了從資料庫中獲取模型列表的資料外,從資料庫獲取模型的一條記錄資料也是常見的需求。比如檢視某篇文章的詳情,就是從資料庫中獲取這篇文章的記錄然後渲染模板。對於這種型別的需求,django 提供了一個 DetailView 類檢視。下面我們就來將 detail 檢視函式轉換為等價的類檢視 PostDetailView,程式碼如下:

blog/views.py

from django.views.generic import ListView,DetailView

# 記得在頂部匯入 DetailView
class PostDetailView(DetailView):
    # 這些屬性的含義和 ListView 是一樣的
    model = Post
    template_name = 'blog/detail.html'
    context_object_name = 'post'

    def get(self,request,*args,**kwargs):
        # 覆寫 get 方法的目的是因為每當文章被訪問一次,就得將文章閱讀量 +1
        # get 方法返回的是一個 HttpResponse 例項
        # 之所以需要先呼叫父類的 get 方法,是因為只有當 get 方法被呼叫後,
        # 才有 self.object 屬性,其值為 Post 模型例項,即被訪問的文章 post
        response = super(PostDetailView,self).get(request,**kwargs)

        # 將文章閱讀量 +1
        # 注意 self.object 的值就是被訪問的文章 post
        self.object.increase_views()

        # 檢視必須返回一個 HttpResponse 物件
        return response

    def get_object(self,queryset=None):
        # 覆寫 get_object 方法的目的是因為需要對 post 的 body 值進行渲染
        post = super().get_object(queryset=None)
        md = markdown.Markdown(extensions=[
            'markdown.extensions.extra','markdown.extensions.codehilite',# 記得在頂部引入 TocExtension 和 slugify
            TocExtension(slugify=slugify),])
        post.body = md.convert(post.body)

        m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>',md.toc,re.S)
        post.toc = m.group(1) if m is not None else ''

        return post
複製程式碼

PostDetailView 稍微複雜一點,主要是等價的 detail 檢視函式本來就比較複雜,下面來一步步對照 detail 檢視函式中的程式碼講解。

首先我們為 PostDetailView 類指定了一些屬性的值,這些屬性的含義和 ListView 中是一樣的,這裡不再重複講解。

緊接著我們覆寫了 get 方法。這對應著 detail 檢視函式中將 post 的閱讀量 +1 的那部分程式碼。事實上,你可以簡單地把 get 方法的呼叫看成是 detail 檢視函式的呼叫。

接著我們又複寫了 get_object 方法。這對應著 detail 檢視函式中根據文章的 id(也就是 pk)獲取文章,然後對文章的 post.body 進行 Markdown 解析的程式碼部分。

你也許會被這麼多方法搞亂,為了便於理解,你可以簡單地把 get 方法看成是 detail 檢視函式,至於其它的像 get_objectget_context_data 都是輔助方法,這些方法最終在 get 方法中被呼叫,這裡你沒有看到被呼叫的原因是它們隱含在了 super(PostDetailView,**kwargs) 即父類 get 方法的呼叫中。最終傳遞給瀏覽器的 HTTP 響應就是 get 方法返回的 HttpResponse 物件。

還是無法理解麼?在不涉及原始碼的情況下我也只能講這麼多了。要想熟練掌握並靈活運用類檢視必須仔細閱讀類檢視的原始碼,我當時也是啃原始碼啃了很久很久,以後我會專門開闢一個專題分析類檢視的原始碼,到時候你就會對類檢視有更深的理解了。此外,這裡是 django 官方檔案對類檢視的講解,儘管我覺得這部分檔案對類檢視也講得不是很清楚,不過也值得作為參考吧 基於類的檢視概述

文章詳情的類檢視也寫好了,同樣的,你需要在 urls.py 中進行配置,將原來的函式檢視 detail 改為類檢視,相信你應該已經知道如何做了。

配置好詳情頁檢視之後,訪問一下文章的詳情,可以看到頁面返回的結果和函式檢視是一模一樣的,至此,類檢視就改造完畢。因為類檢視和函式檢視是完全等價的,而且類檢視具有程式碼複用等很多好處,所以以後一旦涉及檢視,我們都會使用類檢視來實現。


『講解開源專案系列』——讓對開源專案感興趣的人不再畏懼、讓開源專案的發起者不再孤單。跟著我們的文章,你會發現程式設計的樂趣、使用和發現參與開源專案如此簡單。歡迎留言聯絡我們、加入我們,讓更多人愛上開源、貢獻開源~