HelloDjango 第 12 篇:解鎖部落格側欄,GoGoGo!
- 2019 年 10 月 3 日
- 筆記
作者:HelloGitHub-追夢人物
文中涉及的示例程式碼,已同步更新到 HelloGitHub-Team 倉庫
我們的部落格側邊欄有四項內容:最新文章、歸檔、分類和標籤雲。這些內容相對比較固定和獨立,且在各個頁面都會顯示,如果像文章列表或者文章詳情一樣,從視圖函數中獲取這些數據然後傳遞給模板,則每個頁面對應的視圖函數里都要寫一段獲取這些內容的程式碼,這會導致很多重複程式碼。更好的解決方案是直接在模板中獲取,為此,我們使用 django 的一個新技術:自定義模板標籤來完成任務。
使用模板標籤的解決思路
我們前面已經接觸過一些 django 內置的模板標籤,比如比較簡單的 {% static %}
模板標籤,這個標籤幫助我們在模板中引入靜態文件。還有比較複雜的如 {% for %} {% endfor%}
標籤。這裡我們希望自己定義一個模板標籤,例如名為 show_recent_posts
的模板標籤,它可以這樣工作:我們只要在模板中寫入 {% show_recent_posts %}
,那麼模板中就會渲染一個最新文章列表頁面,這和我們在編寫部落格首頁面視圖函數是類似的。首頁視圖函數中從資料庫獲取文章列表並保存到 post_list
變數,然後把這個 post_list
變數傳給模板,模板使用 for 模板標籤循環這個文章列表變數,從而展示一篇篇文章。這裡唯一的不同是我們從資料庫獲取文章列表的操作不是在視圖函數中進行,而是在模板中通過自定義的 {% show_recent_posts %}
模板標籤進行。
以上就是解決思路,但模板標籤不是隨意寫的,必須遵循 django 的規範才能在 django 的模板系統中使用,下面就依照這些規範來實現我們的需求。
模板標籤目錄結構
首先在我們的 blog 應用下創建一個 templatetags 文件夾。然後在這個文件夾下創建一個 __init__.py 文件,使這個文件夾成為一個 Python 包,之後在 templatetags 目錄下創建一個 blog_extras.py 文件,這個文件存放自定義的模板標籤程式碼。
此時你的目錄結構應該是這樣的:
blog __init__.py admin.py apps.py migrations __init__.py models.py static templatetags __init__.py blog_extras.py tests.py views.py
編寫模板標籤程式碼
接下來就是編寫各個模板標籤的程式碼了,自定義模板標籤程式碼寫在 blog_extras.py 文件中。其實模板標籤本質上就是一個 Python 函數,因此按照 Python 函數的思路來編寫模板標籤的程式碼就可以了,並沒有任何新奇的東西或者需要新學習的知識在裡面。
最新文章模板標籤
打開 blog_extras.py 文件,開始寫我們的最新文章模板標籤。
from django import template from ..models import Post, Category, Tag register = template.Library() @register.inclusion_tag('blog/inclusions/_recent_posts.html', takes_context=True) def show_recent_posts(context, num=5): return { 'recent_post_list': Post.objects.all().order_by('-created_time')[:num], }
這裡我們首先導入 template 這個模組,然後實例化了一個 template.Library
類,並將函數 show_recent_posts
裝飾為 register.inclusion_tag
,這樣就告訴 django,這個函數是我們自定義的一個類型為 inclusion_tag 的模板標籤。
inclusion_tag 模板標籤和視圖函數的功能類似,它返回一個字典值,字典中的值將作為模板變數,傳入由 inclusion_tag 裝飾器第一個參數指定的模板。當我們在模板中通過 {% show_recent_posts %}
使用自己定義的模板標籤時,django 會將指定模板的內容使用模板標籤返回的模板變數渲染後替換。
inclusion_tag 裝飾器的參數 takes_context
設置為 True
時將告訴 django,在渲染 _recent_posts.html 模板時,不僅傳入show_recent_posts
返回的模板變數,同時會傳入父模板(即使用 {% show_recent_posts %}
模板標籤的模板)上下文(可以簡單理解為渲染父模板的視圖函數傳入父模板的模板變數以及 django 自己傳入的模板變數)。當然這裡並沒有用到這個上下文,這裡只是做個簡單演示,如果需要用到,就可以在模板標籤函數的定義中使用 context 變數引用這個上下文。
接下來就是定義模板 _recent_posts.html 的內容。在 templatesblogs 目錄下創建一個 inclusions 文件夾,然後創建一個 _recent_posts.html 文件,內容如下:
<div class="widget widget-recent-posts"> <h3 class="widget-title">最新文章</h3> <ul> {% for post in recent_post_list %} <li> <a href="{{ post.get_absolute_url }}">{{ post.title }}</a> </li> {% empty %} 暫無文章! {% endfor %} </ul> </div>
很簡單,循環由 show_recent_posts
傳遞的模板變數 recent_post_list
即可,和 index.html 中循環顯示文章列表是一樣的。
歸檔模板標籤
和最新文章模板標籤一樣,先寫好函數,然後將函數註冊為模板標籤即可。
@register.inclusion_tag('blog/inclusions/_archives.html', takes_context=True) def show_archives(context): return { 'date_list': Post.objects.dates('created_time', 'month', order='DESC'), }
這裡 Post.objects.dates
方法會返回一個列表,列表中的元素為每一篇文章(Post)的創建時間(已去重),且是 Python 的 date
對象,精確到月份,降序排列。接受的三個參數值表明了這些含義,一個是 created_time
,即 Post
的創建時間,month
是精度,order='DESC'
表明降序排列(即離當前越近的時間越排在前面)。例如我們寫了 3 篇文章,分別發佈於 2017 年 2 月 21 日、2017 年 3 月 25 日、2017 年 3 月 28 日,那麼 dates
函數將返回 2017 年 3 月 和 2017 年 2 月這樣一個時間列表,且降序排列,從而幫助我們實現按月歸檔的目的。
然後是渲染的模板 _archives.html 的內容:
<div class="widget widget-archives"> <h3 class="widget-title">歸檔</h3> <ul> {% for date in date_list %} <li> <a href="#">{{ date.year }} 年 {{ date.month }} 月</a> </li> {% empty %} 暫無歸檔! {% endfor %} </ul> </div>
由於 date_list
中的每個元素都是 Python 的 date
對象,所以可以引用 year
和 month
屬性來獲取年份和月份。
分類模板標籤
過程還是一樣,先寫好函數,然後將函數註冊為模板標籤。注意分類模板標籤函數中使用到了 Category
類,其定義在 blog.models.py 文件中,使用前記得先導入它,否則會報錯。
@register.inclusion_tag('blog/inclusions/_categories.html', takes_context=True) def show_categories(context): return { 'category_list': Category.objects.all(), }
_categories.html 的內容:
<div class="widget widget-category"> <h3 class="widget-title">分類</h3> <ul> {% for category in category_list %} <li> <a href="#">{{ category.name }} <span class="post-count">(13)</span></a> </li> {% empty %} 暫無分類! {% endfor %} </ul> </div>
<span class="post-count">(13)</span>
顯示的是該分類下的文章數目,這個特性會在接下來的教程中講解如何實現,目前暫時用佔位數據代替吧。
標籤雲模板標籤
標籤和分類其實是很類似的,模板標籤:
@register.inclusion_tag('blog/inclusions/_tags.html', takes_context=True) def show_tags(context): return { 'tag_list': Tag.objects.all(), }
_tags.html:
<div class="widget widget-tag-cloud"> <h3 class="widget-title">標籤雲</h3> <ul> {% for tag in tag_list %} <li> <a href="#">{{ tag.name }}</a> </li> {% empty %} 暫無標籤! {% endfor %} </ul> </div>
使用自定義的模板標籤
打開 base.html,為了使用剛才定義的模板標籤,我們首先需要在模板中導入存放這些模板標籤的模組,這裡是 blog_extras.py 模組。當時我們為了使用 static 模板標籤時曾經導入過 {% load static %}
,這次在 {% load static %}
下再導入 blog_extras:
templates/base.html {% load static %} {% load blog_extras %} <!DOCTYPE html> <html> ... </html>
然後找到側邊欄各項,將他們都替換成對應的模板標籤:
templates/base.html <aside class="col-md-4"> {% block toc %} {% endblock toc %} {% show_recent_posts %} {% show_archives %} {% show_categories %} {% show_tags %} <div class="rss"> <a href=""><span class="ion-social-rss-outline"></span> RSS 訂閱</a> </div> </aside>
此前側邊欄中各個功能塊都替換成了模板標籤,其實實際內容還是一樣的,只是我們將其挪到了模組化的模板中,並有這些自定義的模板標籤負責渲染這些內容。
此外我們定義的 show_recent_posts
標籤可以接收參數,默認為 5,即顯示 5 篇文章,如果要控制其顯示 10 篇文章,可以使用 {% show_recent_posts 10 %}
這種方式傳入參數。
現在運行開發伺服器,可以看到側邊欄顯示的數據已經不再是之前的佔位數據,而是我們保存在資料庫中的數據了。
注意:
如果你是在開發伺服器啟動的過程中編寫的模板標籤程式碼,那麼一定要重啟一下開發伺服器才能導入 blog_extras,否則會報
TemplateSyntaxError at /
‘blog_extras’ is not a registered tag library. Must be one of:
類似這樣的錯誤。
注意:如果你按照教程的步驟做完後發現報錯,請按以下順序檢查。
- 檢查目錄結構是否正確。確保 templatetags 位於 blog 目錄下,且目錄名必須為 templatetags。具體請對照上文給出的目錄結構。
- 確保 templatetags 目錄下有 __init__.py 文件。
- 確保通過
register = template.Library()
和@register.inclusion_tag
裝飾器將函數裝飾為一個模板標籤。 - 確保在使用模板標籤以前導入了 blog_extras,即 {% load blog_extras%}。注意要在使用任何 blog_extras下的模板標籤以前導入它。
- 確保模板標籤的語法使用正確,即 {% load blog_extras %},注意 { 和 % 以及 % 和 } 之間沒有任何空格。
歡迎關注 HelloGitHub 公眾號,獲取更多開源項目的資料和內容