页面侧边栏:使用自定义模板标签

2019-08-3015810 阅读37 评论

我们的博客侧边栏有四项内容:最新文章、归档、分类和标签云。这些内容相对比较固定和独立,且在各个页面都会显示,如果像文章列表或者文章详情一样,从视图函数中获取这些数据然后传递给模板,则每个页面对应的视图函数里都要写一段获取这些内容的代码,这会导致很多重复代码。更好的解决方案是直接在模板中获取,为此,我们使用 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 对象,所以可以引用 yearmonth 属性来获取年份和月份。

分类模板标签

过程还是一样,先写好函数,然后将函数注册为模板标签。注意分类模板标签函数中使用到了 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:

类似这样的错误。

注意:如果你按照教程的步骤做完后发现报错,请按以下顺序检查。

  1. 检查目录结构是否正确。确保 templatetags 位于 blog 目录下,且目录名必须为 templatetags。具体请对照上文给出的目录结构。
  2. 确保 templatetags 目录下有 __init__.py 文件。
  3. 确保通过 register = template.Library()@register.inclusion_tag 装饰器将函数装饰为一个模板标签。
  4. 确保在使用模板标签以前导入了 blog_extras,即 {% load blog_extras%}。注意要在使用任何 blog_extras下的模板标签以前导入它。
  5. 确保模板标签的语法使用正确,即 {% load blog_extras %},注意 { 和 % 以及 % 和 } 之间没有任何空格。

-- EOF --

37 评论
登录后回复
Feko
2022-08-12 17:45:00

templates\blog
通过+1

回复
icesharp107
2022-08-12 15:38:25

提示TemplateSyntaxError: 'blog_extras' is not a registered tag library. Must be one of:
这需要在setting文件templates 配置library的注册自定义标签
'libraries':{'blog_extras': 'templatetags.blog_extras', },

回复
山林小四
2021-09-07 17:19:32

这个地方有坑踩过:
'date_list': Post.objects.dates('created_time', 'month', order='DESC')

返回的日期列表为空。提取不出来年份月份

需要进入在settings.py 里把USE_TZ改为Flase

USE_TZ = False

回复
kimi
2021-04-21 14:36:08

Post.created_time是DateTimeField,在获取归档信息的时候应该用Post.objects.datetimes吧?

回复
LowSing3721 kimi
2021-04-26 09:27:27

看你的归档分组的精细程度了,文中是以月份分组的所以用dates就够了

回复
zjlclimb
2020-11-10 17:04:16

请问博主,为何我会报错“django.template.exceptions.TemplateDoesNotExist: My_Blog/inclusions/_recent_posts.html
”呀,我的路径的单词拼写都没错,GOOGLE了很多说是修改setting,但是我的setting设置应该是没问题的呀“'DIRS': [os.path.join(BASE_DIR,'templates')]”。
PS:django版本是3.1
求助求助~

回复
追梦人物 zjlclimb
2020-11-11 18:03:21

肯定是路径出了问题,检查一下拼接的完整路径是正确的不?

回复
zjlclimb 追梦人物
2020-11-12 16:12:15

终于解决了!历时两天!原来是我在改文件名的时候,加了一个空格,FUCK!! 谢谢博主,这么短时间就回复我,真心感谢!

回复
Coder
2020-10-19 21:50:04

博主你在给模版传递参数时候用的是通过context字典,怎么没用locals()呢??是不是两者之间还是有比较大的区别。

回复
追梦人物 Coder
2020-10-21 18:13:19

这不是一个好实践。参考 python 之禅,explicit is better than implicit。

回复
Fan()
2020-10-19 15:52:57

你说要是没有博主, 这些东西我自己翻文档得翻到啥时候啊...

回复
徒徒徒2018 Fan()
2021-10-14 16:16:02

真的是最好的入门手册了!

回复
yuanjia123
2020-09-26 12:48:51

_archives.html 是要在blogs/inclusions、下面创建
_archives.html吗

回复
追梦人物 yuanjia123
2020-10-16 23:27:09

是的

回复
iSunnyLee
2020-06-16 23:01:02

请问博主,什么是上下文

回复
追梦人物 iSunnyLee
2020-06-17 11:37:34

Context,可理解为一次请求中携带的各种额外信息。

回复
962447907
2020-01-26 10:36:01

你好我想问下直接在视图函数detail中通过
Category.objects.filter(category__id=post.pk)
这样获取所有分类然后遍历到html中吗,这样的话代码只要写这一句话就可以了

回复
追梦人物 962447907
2020-01-31 11:41:08

Category.objects.filter(category__id=post.pk)

这句代码有问题,过滤 Category 为什么通过文章 post 的 id 来过滤?

回复
shexiaobin
2019-12-29 13:02:48

你好,请问这里出现了这种情况呢
image.png

回复
miaocbin
2019-11-06 11:10:37

大佬,把我遇到的问题都想到了

回复
Be kind to yourself.
2019-10-29 00:18:42

更新:2019年10月29日 00点17分
首先,很感谢作者写这些教程。

有个问题,想与你商榷下哈。
“接下来就是定义模板 _recent_posts.html 的内容。在 templates\blogs 目录下创建一个 inclusions 文件夹,然后创建一个 _recent_posts.html 文件,内容如下:”
中的 “templates\blogs” 模板的路径是不是错了?为templates\blog,才能运行成功。

谢谢!

回复
追梦人物 Be kind to yourself.
2019-10-30 15:36:36

是的,笔误,感谢指出。

回复
Be kind to yourself.
2019-10-28 12:24:21

首先,很感谢作者写这些教程。

有个问题,想与你商榷下哈。
“接下来就是定义模板 _recent_posts.html 的内容。在 templates\blogs 目录下创建一个 inclusions 文件夹,然后创建一个 _recent_posts.html 文件,内容如下:”
模板的路径是 HelloDjango-blog\templates\blog\inclusions_archives.html,才能运行成功。

谢谢!

回复
好汉源6475728697
2019-10-06 20:51:07

请问__init__.py文件中有内容吗?

回复
追梦人物 好汉源6475728697
2019-10-09 06:52:29

没有

回复
好汉源6475728697
2019-10-06 20:13:28

UnicodeDecodeError at /
'utf-8' codec can't decode byte 0xd7 in position 69: invalid continuation byte
请问这个报错是为什么啊

回复
追梦人物 好汉源6475728697
2019-10-09 06:53:17

如果是 Python3 的话,应该是文件编码格式不对,重新保存为 utf-8 试一下。

回复
叶落知秋
2019-09-23 15:24:08

TemplateDoesNotExist at /
blog/inclusions/_recent_posts.html

这个报错是为什么啊

回复
追梦人物 叶落知秋
2019-09-23 17:11:00

模板没有找到,检查一下模板路径对不对。

回复
叶落知秋 追梦人物
2019-09-24 09:20:11

好的

回复
xlb233
2019-09-11 01:22:06

大佬,@register.inclusion_tag('blog/inclusions/_recent_posts.html', takes_context=True),这个装饰器是取的blogproject/templates文件夹作为默认路径啊,导致报如图的错误image.png是还需要配置一下默认路径为blog应用下的路径吗?django版本是2.2.5,请大佬指教一下

回复
xlb233 xlb233
2019-09-11 01:22:45

image.png

回复
xlb233 xlb233
2019-09-11 01:23:18

额,图片发不上来,报错如下
django.template.loaders.filesystem.Loader: C:\Users\Admin\Desktop\myblog\blogproject\templates\blog\inclusions_archives.html (Source does not exist)

回复
追梦人物 xlb233
2019-09-11 07:27:50

你的templates文件夹位置可能放错了吧。项目中的配置是,templates 位于项目根目录。

回复
xlb233 追梦人物
2019-09-11 09:45:57

templates没有放错啊,就放在manage.py的同级目录下的,有点迷惑了。。。

回复
追梦人物 xlb233
2019-09-11 10:12:16

从报错来看,你设置中模板路径应该是 myblog\blogproject,但我猜测你的 templates 目录应该在 myblog 下吧?所以检查一下设置中templates设置的路径。

回复
xlb233 追梦人物
2019-09-11 23:03:12

我傻了,我把inclusion文件夹放错目录了,目前问题已解决,多谢大佬!

回复