Django 官方推薦的姿勢:類視圖
- 2019 年 10 月 25 日
- 筆記
文中所涉及的示例程式碼,已同步更新到 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('', views.index, 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(), name='index'), ... ]
訪問一下首頁,可以看到首頁依然顯示全部文章列表,和使用視圖函數 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, pk=self.kwargs.get('pk')) return super(CategoryView, 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, *args, **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_object
、get_context_data
都是輔助方法,這些方法最終在 get
方法中被調用,這裡你沒有看到被調用的原因是它們隱含在了 super(PostDetailView, self).get(request, *args, **kwargs)
即父類 get
方法的調用中。最終傳遞給瀏覽器的 HTTP 響應就是 get
方法返回的 HttpResponse
對象。
還是無法理解么?在不涉及源碼的情況下我也只能講這麼多了。要想熟練掌握並靈活運用類視圖必須仔細閱讀類視圖的源碼,我當時也是啃源碼啃了很久很久,以後我會專門開闢一個專題分析類視圖的源碼,到時候你就會對類視圖有更深的理解了。此外,這裡是 django 官方文檔對類視圖的講解,儘管我覺得這部分文檔對類視圖也講得不是很清楚,不過也值得作為參考吧 基於類的視圖概述。
文章詳情的類視圖也寫好了,同樣的,你需要在 urls.py 中進行配置,將原來的函數視圖 detail
改為類視圖,相信你應該已經知道如何做了。
配置好詳情頁視圖之後,訪問一下文章的詳情,可以看到頁面返回的結果和函數視圖是一模一樣的,至此,類視圖就改造完畢。因為類視圖和函數視圖是完全等價的,而且類視圖具有程式碼復用等很多好處,所以以後一旦涉及視圖,我們都會使用類視圖來實現。
『講解開源項目系列』——讓對開源項目感興趣的人不再畏懼、讓開源項目的發起者不再孤單。跟著我們的文章,你會發現編程的樂趣、使用和發現參與開源項目如此簡單。歡迎留言聯繫我們、加入我們,讓更多人愛上開源、貢獻開源~