Loading

django基础之Django模板

Web模板系统

一个模板的渲染需要三方参与:

  • 模板引擎: 主要参与模板渲染的系统
  • 内容源: 输入的数据。比较常见的有数据库、XML文件和用户请求这样的网络数据
  • 模板: 一般是和语言相关的文本

模板系统的工作过程如图所示

由图可知, 模板和内容源由模板引擎处理和组合, 批量生成Web文档。

 

模板介绍

在之前的章节中, 视图函数只是直接返回文本, 而在实际生产环境中其实很少这样用, 因为实际的页面大多是带有样式的HTML代码, 这可以让浏览器渲染出非常漂亮的页面。目前市面上有非常多的模板系统, 其中最知名最好用的就是DTL和jinja2。DTLDjango Template Language三个单词的缩写, 也就是django自带的模板语言。当然也可以配置Django支持jinja2等其他模板引擎, 但是作为Django内置的模板语言, 和Django可以达到无缝衔接而不会产生一些不兼容的情况

DTL与普通的HTML的区别

DTL模板是一种带有特殊语法的HTML文件, 这个HTML文件可以被Django编译, 可以传递参数进去, 实现数据动态化。在编译完成后, 生成一个普通的HTML文件, 然后发送给客户端

渲染模板

渲染模板有多种方式, 这里讲下两种常见的方式

1. render_to_string: 找到模板, 然后将模板编译后渲染成Python的字符串格式。最后再通过HttpResponse类包装成一个HttpResponse对象返回回去。示例代码如下:

from django.template.loader import render_to_string
from django.http import HttpResponse

def book_detail(request, book_id):
    html = render_to_string('detail.html')
    return HttpResponse(html)

2. django还提供了一个更加简便的方式, 直接将模板渲染成字符串和包装成HttpResponse对象一步到位完成。示例代码如下:

from django.shortcuts import render

def book_list(request):
    return render(request, 'list.html')

模板查找路径

在项目的settings.py文件中, 有一个TEMPLATES配置, 这个配置包含了模板引擎的配置, 模板查找路径的配置, 模板上下文配置等。模板路径可以在两个地方配置。

1. DIRS: 这是一个列表, 在这个列表中可以存放所有的模板路径, 以后在视图中使用render或者render_to_string渲染模板的时候, 会在这个列表的路居中查找模板

2. APP_DIRS: 默认为True, 这个设置为True后, 会在INSTALLED_APPS的安装了的APP下的templates文件夹中查找模板。

3. 查找顺序: 比如代码render('list.html')。先会DIRS在这个列表中依次查找路径下有没有这个模板,如果有,就返回。如果DIR列表中所有的路径都没有找到, 那么会先检查当前这个视图所处的app是否已经安装, 如果已经安装了, 那么就先在当前这个app下的templates文件夹中查找模板, 如果没有找到, 那么会在其他已经安装了的app中查找。如果所有路径下都没有找到,那么会抛出一个TemplatesDoesNotExist 的异常

 

模板变量

DTL模板语法

模板中可以包含变量, Django在渲染模板的时候, 可以传递变量对应的值去进行替换。变量的命名规范和Python非常类似, 只能是阿拉伯数字和英文字符以及下划线的组合, 不能出现标点符号等特殊字符。变量需要通过视图函数渲染, 视图函数是在使用render或者render_to_string的时候可以传递一个context的参数, 这个参数是一个字典类型。以后在模板中的变量就从这个字典中读取值的。示例代码如下:

# profile.html模板代码
<p>{{ username }}</p>

# views.py代码
def profile(request):
    return render(request, 'profile.html', context={'username':'zhangjie'})

模板中的变量也支持点(.)的形式。在出现了点的情况, 比如person.username, 模板是按照以下方式进行解析的:

1. 如果person是一个字典, 那么就会查找这个字典的username这个key对应的值

2. 如果person是一个对象, 那么就会查找这个对象的username属性, 或者是username这个方法

3. 如果出现的是person.1, 会判断persons是否是一个列表或者元组或者任意可以通过下标访问的对象, 如果是的话就获取这个列表的第一个值。如果不是就获取到的是一个空的字符串

不能通过中括号的形式访问字典和列表中的值, 比如dict['key']和list[1]是不支持的!

因为使用点(.)语法获取对象的时候, 可以获取这个对象的属性, 如果这个对象是一个字典, 也可以获取这个字典的值, 因此不能在字典中定义字典本身就有的属性当做key, 否则字典的那个属性将变成字典中的key了。

 

常用标签

if标签

if标签相当于Python中的if语句, 有elseelif相对应, 但是所有的标签都需要用标签符号({%%})进行包裹。if标签中可以使用==、!=、<、<=、>、>=、in、not in、is、is not等判断运算符。示例代码如下

{% if '张三' in persons %}
    <p>张三</p>
{% else %}
    <p>李刚</p>
{% endif %}

for...in...标签

for...in...类似于Python中的for...in...。可以遍历列表、元组、字符串、字典等一切可以遍历的对象。示例代码如下:

{% for person in persons %}
    <p>{{ person.name }}</p>
{% endfor %}

如果想要反向遍历, 那么在遍历的时候就加上一个reversed。示例代码如下:

{% for person in persons reversed %}
    <p>{{ person.name }}</p>
{% endfor %}

遍历字典的时候, 需要使用itemskeysvalues等方法。在DTL中, 执行一个方法不能使用圆括号的形式。遍历字典的代码如下:

{% for key, value in persons.item %}
    <p>key:{{ key }}</p>
    <p>value:{{ value }}</p>
{% endfor %}

for循环中, DTL提供了一些变量可供使用。这些变量如下:

  • forloop.counter: 当前循环的下标, 以1作为起始值

  • forloop.counter0: 当前循环的下标, 以0作为起始值

  • forloop.revcounter: 当前循环的反向下标值。比如列表有5个元素, 那么第一次遍历这个属性是等于5, 第二次是4, 依次类推。并且是以1作为最后一个元素的下标

  • forloop.revcounter0: 类似于forloop.counter。不同的是最后一个元素的下标是从0开始

  • forloop.first: 是否第一次遍历

  • forloop.last: 是否是最后一次遍历

  • forloop.parentloop: 如果有多个循环嵌套, 那么这个属性代表的是上一级的for循环 

模板中的for...in...标签没有continue和break语句, 这一点和Python中有很大的不同, 一定要记清楚!

for...in...empty标签

这个标签使用跟for...in是一样的。只不过是在遍历的对象如果没有元素的情况下, 会执行empty中的内容。示例代码如下:

{% for person in persons reversed %}
    <p>{{ person.name }}</p>
{% empty %}
    <p>{{ 暂时还没有人 }}</p>
{% endfor %}

with标签

在模板中定义变量。有时候一个变量访问的时候比较复杂, 那么可以先把这个变量缓存到一个变量上, 以后就可以直接使用这个遍历就可以了。示例代码如下:

context = {
    'person': ['张三', '李四']
}

{% with lisi=person.1 %}
    <p>{{ lisi }}</p>
{% endwith %}

有几点需要强烈注意:

  • with语句中定义的变量, 只能在{% with %}{% endwith %}中使用, 不能在这个标签外面使用。

  • 定义变量的时候, 不能再等号左右两边留有空格。比如{% with lisi = person.1 %}是错误的

  • 还有另外一种写法也同样支持

{% with person.1 as lisi %}
    <p>{{ lisi }}</p>
{% endwith %}

url标签

在模板中, 我们经常要写一些url, 比如某个a标签中需要定义href属性。 当然如果通过硬编码的方式直接将这个url写死在里面也是可以的。但是这样对于以后项目维护可能不是一件好事。因此建议使用这种反转的方式来实现, 类似于django中的reverse一样。示例代码如下:

<a href="{% url 'book:list' %}">图书列表页面</a>

如果url反转的时候需要传递参数, 那么可以在后面传递。但是参数分位置参数和关键字参数。位置参数和关键字参数不能同时使用。示例代码如下:

# path部分
path('detail/<book_id>/', view.book_detail, name='detail')

# url反转, 使用位置参数
<a href='{% url "book:detail" 1 %}'>图书详情页面</a>

# url反转, 使用关键字参数
<a href="{% url 'book:detail' book_id=1 %}">图书详情页面</a>

如果想要在使用url标签反转的时候要传递查询字符串的参数, 那么必须要手动在后面添加。示例代码如下:

<a href='{% url "book:detail" 1 %}?page=1'>图书详情页面</a>

如果需要传递多个参数, 那么通过空格的方式进行分隔。示例代码如下:

<a href="{% url 'book:detail' book_id=1 page=2 %}">图书详情页面</a>

spaceless标签

移除html标签中的空白字符。包括空格、tab键、换行等。示例代码如下:

{% spaceless %}
    <p>
        <a href='foo/'>foo</a>
    </p>
{% endspaceless %}

那么在渲染完成后, 会变成以下的代码:

<p><a href='foo/'>foo</a></p>

spaceless只会移除html标签之间的空白字符, 而不会移除标签与文本之间的空白字符。

{% spaceless %}
    <strong>
        Hello 
    </strong>
{% endspaceless %}

这个将不会移除strong中的空白字符

autoescape标签

开启和关闭这个标签内元素的自动转义功能。自动转义是可以将一些特殊的字符。比如<转义成html语法能识别的字符, 比如<会被转义成&lt;, 而>会被转义成&gt;。模板中默认是已经开启了自动转义的。autoescape的示例代码如下

# 传递的上下文信息
context = {
    'info':'<a href="www.baidu.com">百度</a>',
}

# 模板中关闭自动转义
{% autoescape on %}
    {{ info }}
{% endautoescape %}

那么就会显示百度的一个超链接。如果把on换成off, 那么就会显示成一个普通的字符串。示例代码如下:

{% autoescape off%}
    {{ info }}
{% endautoescape %}

verbatim标签

默认在DTL模板中是会去解析哪些特殊字符的。比如{%%}以及{{之间}}等。如果你再某个代码片段中不想使用DTL的解析引擎。那么可以把这个代码片段放在verbatim标签中。示例代码如下:

{% verbatim %}
    {{if dying}}Still alive.{{/if}}
{% endverbatim%}

更多标签请参考官方文档: https://docs.djangoproject.com/en/2.0/ref/templates/builtins

 

常用过滤器

在模板中, 有是有需要对一些数据进行处理以后才能使用。一般在Python中我们是通过函数的形式来完成的。而在模板中, 则是通过过滤器来实现的。过滤器使用的是|来使用。比如使用add过滤器, 那么示例代码如下:

{{ value|add:"2" }}

add

将传进来的参数添加到原来的值上面。这个过滤器会尝试将参数转换成整形然后进行相加。如果转换成整形的过程中失败了, 那么就会将参数进行拼接。如果是字符串, 那么就会拼接成字符串。如果是列表, 那么就会拼接成一个列表。示例代码如下:

{{ value|add:"2" }}

如果value是等于4, 那么结果将是6, 如果value是等于一个普通的字符串, 比如abc, 那么结果将是abc2add过滤器的源码如下:

def add(value, arg):
    """Add thr arg to the value."""
    try:
        return int(value) + int(arg)
    except (ValueError, TypeError):
        try:
            return value+arg
        exceptreturn ''

cut

移除值中所有指定字符串, 类似于Python中的replace(arg, "")。示例代码如下:

{{ value|cut:" "}}

以上示例将会移除value中所有的空格字符串。cut过滤器的源代码如下:

def cut(value, arg):
    """ Remove all values of arg from the given string."""
    safe = isinstance(value, SafeData)
    value = value.replace(arg, '')
    if safe and arg != ';':
        return mark_safe(value)
    return value

date

将一个日期按照指定的格式, 格式化成字符串。示例代码如下:

# 数据
context = {
    'birthday': datetime.now(),
}

# 模板
{{ birthday|date: "Y/m/d" }}

那么将会输出2018/02/01。其中Y代表的是四位数字的年份。m代表的是两位数字的月份。d代表的是两位数字的日。还有更多时间格式化的方式。见如下表

格式字符描述示例
Y 四位数字的年份 2018
m 两位数字的月份 01-12
n 月份, 1-9之前没有0前缀 1-12
d 两位数字的天 01-31
j 天, 但是1-9前面没有0前缀 1-31
g 小时, 12小时格式的, 1-9之前没有0前缀 1-12
h 小时, 12小时格式的, 1-9之前有0前缀 01-12
G 小时, 24小时格式的, 1-9之前没有0前缀 1-23
H 小时, 24小时格式的, 1-9之前有前缀 01-23
i 分钟, 1-9之前有0前缀 00-59
s 秒, 1-9之前有0前缀 00-59

default

如果值被评估为False。比如[]""None{}等这些在if判断中为False的值, 都会使用default过滤器提供的默认值。示例代码如下:

{{ value|default:'nothing' }}

如果value是等于一个空的字符串。比如"", 那么以上代码将会输出nothing

default_if_none

如果值是None, 那么将会使用default_if_none提供的默认值。这个和default有区别, default是所有评估为False的都会使用默认值。而default_if_none则只有这个值是等于None的时候才会使用默认值。示例代码如下:

{{ value|default:"nothing" }}

如果value是等于""也即空字符串, 那么以上将会输出空字符串。如果value是一个None值,以上代码才会输出nothing

first

返回列表/元组/字符串中的第一个元素。示例代码如下:

{{ value|first }}

如果value是等于['a', 'b', 'c'], 那么输出将会是a

last

返回列表/元组/字符串的最后一个元素。示例代码如下:

{{ value|last }}

如果value是等于['a', 'b', 'c'], 那么输出将会是c

floatformat

使用四舍五入的方式格式化一个浮点类型。如果这个过滤器没有传递任何参数。那么只会在小数点后保留一个小数, 如果小数后面全是0, 那么只会保留整数。当然也可以传递一个参数, 标识具体要保留几个小数。

1. 如果没有传递参数

value模板代码输出
34.23234 {{ value|floatformat}} 34.2
34.000 {{ value|floatformat }} 34
34.260 {{ value|floatformat }} 34.3

2. 如果传递参数

value模板代码输出
34.23234 {{ value|floatformat:3}} 34.232
34.00000 {{value|floatformat:3}} 34.000
34.26000 {{value|floatformat:3}} 34.260

join

类似于Python中的join, 将列表/元组/字符串用指定的字符进行拼接。示例代码如下:

{{ value|join '/'}}

如果value是等于['a', 'b', 'c'], 那么上面代码将输出a/b/c

length

获取一个列表/元组/字符串/字典的长度。示例代码如下:

{{ value|length }}

如果value是等于['a', 'b', 'c'], 那么上面代码将输出3。如果valueNone, 那么以上将返回0

lower

将值中所有的字符全部转换成小写。示例代码如下: 

{{ value|lower }}

upper

将值中所有的字符全部转换成小写。示例代码如下:

{{ value|upper }}

如果value是等于Hello World。那么以上代码将输出HELLO WOLRD

random

在被给的列表/字符串/元组中随机选择一个值。示例代码如下:

{{ value|random }}

如果value是等于['a', 'b', 'c'], 那么以上代码会在列表中随机选择一个

safe

标记一个字符串是安全的。也即会关掉这个字符串的自动转义。示例代码如下:

{{ value|safe }}

如果value是一个不包含任何特殊字符的字符串, 比如<a>这种, 那么以上代码将会把字符串正常输入。如果value是一串html代码, 那么以上代码将会把这个html代码渲染到浏览器中

slice

类似于Python中的切片操作。示例代码如下:

{{ some_list|slice:'2:'}}

以上代码将会给some_list2开始做切片操作

stringtags

删除字符串中所有的html标签。示例代码如下:

{{ value|stringtags }}

如果value<strong>hello world</strong>, 那么以上代码将会输出hello world

truncatechars

如果给定的字符串长度超过了过滤器指定的长度。那么将会进行切割, 并且会拼接三个点来作为省略号。示例代码如下:

{{ value|truncatechars:5 }}

如果value等于北京欢迎您~, 那么输出结果是北京...

truncatechars_html

类似于truncatechars, 只不过是不会切割html标签。示例代码如下:

{{ value|truncatechars_html }}

如果value等于<p>北京欢迎您~</p>, 那么输出结果是<p>北京...</p>

 

自定义过滤器

自定义过滤器介绍

虽然DTL给我们内置了许多好用的过滤器。但是有些时候还是不能满足我们的需求。因此Django给我们提供了一个接口, 可以让我们自定义过滤器, 实现自己的需求

模板过滤器必须要放在app中, 并且这个app丙戌要在INSTALLED_APPS中进行安装。然后在这个app下面创建一个Python包叫做templatetags。再在这个包下面创建一个python文件。比如app的名字叫做book, 那么项目结构如下:

- book
    - views.py
    - urls.py
    - models.py
    -templatetags
        - my_filter.py

在创建了存储过滤器的文件后, 接下来就是在这个文件中写过滤器了。过滤器实际上就是python中的一个函数, 只不过把这个函数注册到模板库中, 以后就可以使用这个函数了。但是这个函数的参数有限制, 第一个参数必须是这个过滤器需要处理的值, 第二个参数可有可无, 如果有, 那么就意味着在模板中可以传递参数。并且过滤器的函数最多只能有两个参数。在写完过滤器后, 再使用django.template.Library对象注册进去。示例代码如下:

from django import template

# 创建模板库对象
register = template.Library()

# 过滤器函数
def mycut(value, mystr):
    return value.replace(mystr)

# 将函数注册到模板库中
register.filter('mycut', mycut)

以后想要在模板中使用这个过滤器, 就要在模板中load以下这个过滤器所在的模块的名字(也就是这个python文件的名字)。示例代码如下:

{% load my_filter %}

自定义时间过滤器

有时候经常会在朋友圈、微博中可以看到一条信息发表的时间, 并不是具体的时间, 而是距离现在多久。比如刚刚1分钟前等。这个功能DTL是没有内置这样的过滤器的, 因此我们可以自定义一个这样的过滤器。示例代码如下:

# time_filter.py文件
from datetime import datetime
from django import template

register = template.Library()

def time_since(value):
    if isinstance(value, datetime):
        now = datetime.now()
        timestamp = (now-value).total_seconds()
        if timestamp < 60:
            return '刚刚'
           elif timestamp < 60*60:
            minutes = int(timestamp/60)
            return '%s分钟前'%minutes
        elif timestamp < 60 * 60 * 24:
            hours = int(timstamp/(60*60))
            return '%s小时前'%hours
        elif timestamp < 60 * 60 * 24 * 30:
            days = int(timestamp/(60*60*24))
            return '%s天前'%days
        else:
            return value.strftime('%Y/%m/%d %H:%M')
    else:
        return value

register.filter('time_since', time_since)

在模板中使用的示例代码如下:

{% load time_filter %}
...
{% value|time_since %}
...

为了更加方便的将函数注册到模板库中当做过滤器。也可以使用装饰器来讲一个函数包装成过滤器。示例代码如下:

from django import template

register = template.Library()

@register.filter(name='mycut')
def mycut(value, mystr):
    return value.replace(mystr, '')

 

模板结构优化

引入模板

有时候一些代码在许多模板中都用到的。如果我们每次重复的去拷贝代码那肯定不符合项目的规范。一般我们可以把这些重复的代码抽取出来。就类似于Python中的函数一样, 以后想要使用这些代码的时候, 就通过include包含进来。这个标签就是include。示例代码如下:

# header.html
<p>我是header</p>

# footer.html
<p>我是footer</p>

# main.html
{% include 'header.html' %}
<p>我是main内容</p>
{% include 'footer.html' %}

include标签寻找路径的方式, 也是跟render渲染模板的函数是一样的

默认include标签包含模板, 会自动的使用主模板中的上下文, 也即是自动的使用主模板中的变量。如果想要传入一些其他的参数, 那么可以使用with语句, 示例代码如下:

# header.html
<p>用户名: {{username}}</p>
    
# main.html
{% include 'header.html' with username='feather' %}

模板继承

在前端页面开发中。有些代码是需要重复使用的。这种情况可以使用include标签来实现。也可以使用另一种比较强大的方式来实现, 那就是模板继承。模板继承类似于Python中的类, 在父类中定义可以先定义一些变量和方法, 然后在子类中实现。模板继承也可以在父模板中定义好一些子模板需要用到的代码, 然后子模板直接继承就可以了。并且因为子模板肯定有自己不同的代码, 因此可以在父模板中定义一个block接口, 然后子模板再去实现。以下是父模板的代码:

{% load static %}
<!DOCTYPE html>
<html lang='en'>
<head>
    <link rel="stylesheet" href="{% static 'style.css' %}"/>
    <title>{% block title%}我的站点{% endblock %}</title>
</head>

<body>
    <div id="sidebar">
        {% block sidebar %}
        <ul>
            <li><a href="/">首页</a></li>
            <li><a href="/blog/">博客</a></li>
        </ul>
        {% endblock %}
    </div>
    <div id="content">
        {% block content %}{% endblock %}
    </div>
</body>
</html>

这个模板, 我们取名为base.html, 定义好一个简单的html骨架, 然后定义好两个block接口, 让子模板来根据具体需求来实现。子模板然后通过extends标签来实现, 示例代码如下:

{% extends 'base.html' %}

{% block title %}博客列表{% endblock %}

{% block content %}
    {% for entry in blog_entries %}
        <h2>{{ entry.title }}</h2>
        <p>{{ entry.body }}</p>
    {% endfor %}
{% endblock %}

注意:

1. extends标签必须放在模板的第一行。

2. 子模板的代码必须放在block中, 否则不会被渲染

如果在某个block中需要使用父模板的内容, 那么可以使用{{block.super}}来继承。比如上例, {% block title %}, 如果想要使用父模板的title, 那么可以在子模板的title block中使用{{block.super}}来实现

在定义block的时候, 除了在block开始的地方定义这个block的名字, 还可以在block结束的时候定义名字。比如{% block title %}{% endblock title %}。这在大型项目模板中显得尤其有用, 能让你快速的看到block包含在那里

 

加载静态文件

在一个网页中。不仅仅只有一个html的骨架, 还需要css样式文件, js执行文件以及一些图片等。因此在DTL中加载静态文件是一个必须要解决的问题。在DTL中, 使用static标签, 首先需要{% load static %}。加载静态文件的步骤 如下:

1. 首先确保django.contrib.staticfiles已经添加到settings.INSTALLED_APPS

2. 确保在settings.py中设置STATIC_URL

3. 在已经安装了的app中创建一个文件夹叫做static, 然后在这个static文件夹下创建了一个当前app的名字的文件夹, 再把静态文件放到这个文件夹下。例如你的appbook, 有一个静态文件叫做app.png, 那么路径为book/static/book/aa.png(为什么在app下创建一个static文件夹, 还需要在这个static下创建一个同app名字的文件夹呢?原因是如果直接把静态文件放到static文件夹下, 那么在模板加载静态文件的时候就是使用aa.png, 如果多个在多个app之间有同名的静态文件, 这时候就很容易产生混淆。而在static文件夹下创建一个同名app的文件夹, 在模板中加载的时候就是使用book/aa.png, 这样就可以避免混淆了)

4. 如果有一些静态文件是不和任何app挂钩的。那么可以在setting.py中添加STATICFILES_DIRS, 以后DTL就会在这个列表中查找静态文件。比如说可以设置:

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static')
]

5. 在模板中使用load标签加载static标签。比如要加载在项目的static文件夹下的style.css的文件, 那么示例代码如下:

{% load static %}
<link rel="styleheet", href="{% static "style.css" %}">

6. 如果不想每次在模板中加载静态文件都使用load加载static标签, 那么可以在settings.py中的TEMPLATES/OPTIONS添加'builtins':['django.templatetags.static'], 这样以后在模板中就可以直接使用static标签, 而不用手动的load

7. 如果没有在settings.INSTALLED_APPS中添加django.contrib.staticfiles。那么我们就需要手动的将请求静态文件的url与静态文件的路径进行映射了。示例代码如下:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # 其他的url映射
]+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

 

posted @ 2020-08-07 09:33  愚者丶  阅读(484)  评论(0编辑  收藏  举报