作者: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 的內容。在 templates\blogs 目錄下創建一個 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 公眾號,獲取更多開源項目的資料和內容