第四节:模板层

模板层

一、模板简介

在刚刚介绍完的视图层中我们提到,浏览器发送的请求信息会转发给视图进行处理,而视图在经过一系列处理后必须要有返回信息给浏览器。如果我们要返回html标签、css等数据给浏览器进行渲染,我们可以在视图中这么做

from django.shortcuts import HttpResponse
import time

# 返回静态内容(静态页面):页面内容固定不变
def index(request):
    html = "<html><body><h1>静态内容,固定不变</h1></body></html>"
    return HttpResponse(html)

# 返回动态内容(动态页面):后台处理会用变量值填充内容,每次得到的内容都可能不同
def current_datetime(request):
    now_time = time.strftime('%Y-%m-%d %X')
    html = "<html><body><h1>动态内容,%s</h1></body></html>" % now_time
    return HttpResponse(html)

上例所示,我们直接将HTML代码放到视图里,然后进行返回,这可以使我们很直观地看清楚浏览器从发送请求到看到前端界面内容的这个过程中视图的基本工作原理,但是这种将前端代码与后端代码完全耦合到了一起开发方式会使得程序的可维护性与可扩展性变差

前端界面一旦需要重新设计、修改,则必须对后端的Python代码进行相应的修改。 然而前端界面的修改往往比后端 Python 代码的修改要频繁得多,因此如果可以在不修改 Python 代码的情况下变更前端界面的设计,那将会方便得多。

我们可以很容易想到的解决方案就是

# 1、首先将前端代码放入单独的HTML文件中
# 2、然后编写查找/加载这些文件的统一方法/API(否则需要在每个视图里重复编写查找/加载的代码)。

作为一个成熟的Web框架,上述方案早已被Django实现:

# 1、Django提供了模板系统 (Template System)用来专门定制html文件,一个html文件称之为一个模板
    对于静态页面来说,直接编写就好
    而针对动态页面,django额外提供了专门的模板语言(Django template language,简称DTL),允许我们在页     
    面中嵌入模板变量,这便为后期为页面动态填充内容提供了可能性。
    DTL是模板系统的核心,因此django的模板系统也被等同于DTL

    详见第三小节

# 2、Django定义了一套标准的API用来查找/加载(读取并进行预处理称之为加载)并且渲染模板
    渲染rendering指的是用上下文数据context data插入/填充模板并返回结果,结果为字符串。
    将要插入/填充入模板的变量组织到一个字典里,该字典称之为上个文context data

    详见第二小节->2.1

一个简单的示例如下

templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>静态内容,固定不变</h1>

</body>
</html>

templates/current_datetime.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>动态内容,{{ now }}</h1>

</body>
</html>

views.py

from django.shortcuts import render
import time

# 返回静态内容(静态页面)
def index(request):
    return render(request,'index.html')

# 返回动态内容(动态页面)
def current_datetime(request):
    now_time = time.strftime('%Y-%m-%d %X')
    context={"now":now_time}
    return render(request,'current_datetime.html',context)

urls.py

from django.urls import path
from app01.views import *

urlpatterns = [
    path('index/', index),
    path('current_datetime/', current_datetime),
]

二、模板的使用

1、标准API

Django内置了一套标准API(兼容任何模板引擎的BACKEND)用于查找/加载、渲染模板。历史原因,模板的相关实现都存在于django.template名称空间中。

# 例如
# 1、django.template.loader 加载器,定义了函数get_template与select_template用于模板的查找
get_template(template_name, using=None)
select_template(template_name_list, using=None)

# 2、django.template.backends 模板引擎后端实现
django.template.backends.django.DjangoTemplates
django.template.backends.jinja2.Jinja2

# 3、Template.render(context=None, request=None) 用于渲染模板
from django.template.backends.django import Template
from django.template.backends.jinja2 import Template

# 4、django.template.loader.render_to_string 是一个快捷方法,内部就是调用1和3的API
render_to_string(template_name, context=None, request=None, using=None)

# 若想深入了解,可以参照官网来阅读源码https://docs.djangoproject.com/en/3.0/topics/templates/#usage

上述API了解即可,因为我们在日常开发过程中,常用的一个render方法(from django.shortcuts import render),其内部已经整合了上述API。

from django.shortcuts import render

#  通过查看源码会发现render内部就是在调用上述API
render(request, template_name, context=None, content_type=None, status=None, using=None)
render参数介绍,详细使用见下一小节 1、request就是一个HttpRequest对象

2、template_name:
    可以是一个单独的模板
        'story_detail.html'
        'news/story_detail.html'

    也可以是一个模板列表
        ['story_1_detail.html', 'story_detail.html']
3、context:
    是一个字典,包含了将要插入/填充入模板的变量,称之为上个文数据(context data)

4、content_type
    指定响应的内容类型,如content_type='text/plain; charset=utf-8'

5、status:
    指定响应的状态码,如status=200

6、using:
    指定使用的模板引擎名字,如using='name1',代表使用名为name1的模板引擎

2、模板引擎配置

要解析DTL,需要有专门的解析器,称之为template engine模板引擎,需要在settings.py中配置TEMPLATES列表,其默认值为空,但在创建项目时会自动生成如下配置

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

列表中一个字典就是一个引擎配置,一个django项目可以配置一个或多个模板引擎(当然,如果你不需要使用模板,也可以不配置),如下

TEMPLATES = [
    # 引擎配置一
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  
        'APP_DIRS': True,
        'OPTIONS': {
            # ... 同上,篇幅问题,下述带有同上字样的请读者在测试时自行填充上述代码 ...
        },
    },

    # 引擎配置二
    {
        'BACKEND': 'django.template.backends.jinja2.Jinja2', 
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  
        'APP_DIRS': True,
        'OPTIONS': {
            # ... 同上 ...
        },
    },
]

(1)关键配置项之BACKEND:

每个引擎都需要有专门的后端程序BACKEND,BACKEND的值为要导入的python类,Django为自己的模块系统以及非常受欢迎的Jinja2模板系统内置了后端

django.template.backends.django.DjangoTemplates

django.template.backends.jinja2.Jinja2

而其他模板语言的后端需要从第三方获得。

#1、基于安全性考虑,最好不要使用不知名作者开发的第三方模板系统
#2、如果你不是必须选择其他的模板系统,还是推荐使用DTL

(2)关键配置项之DIRS与APP_DIRS

由于大多数引擎加载的模板都是文件,这就涉及到文件的路径查找,所以在每个引擎配置的顶级都包含两个相似的配置来专门负责路径查找问题

'DIRS': [], 
1、列表中包含一系列的目录,引擎会按照列表的顺序依次查找模板文件
2、列表为空则代表没有对应的查找目录

'APP_DIRS': True, # 默认为False
1、APP_DIRS 值为True时,会去按照INSTALLED_APPS = []中注册的app顺序,依次去每个app目录下的templates目录中查找模板文件
2、APP_DIRS 值为False时,不会检索app目录下的templates目录
 查找的优先级为: djanog会先依次检索每个引擎下的DIRS,然后按照APP_DIRS的规定去找模板,直到找到为止。下面我们就依次举例进行验证
 我们先将ARR_DIRS设置为False来讨论加载顺序,如下
TEMPLATES = [
    {
        # django引擎
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/home/html/example.com',
            '/home/html/default',
        ],
        'APP_DIRS': False, # 关闭去每个APP下查找,不设置默认值为False
        'OPTIONS': {
            # ... 同上 ...
        },
    },
    {
        # jinja2引擎
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [
            '/home/html/jinja2',
        ],
        'APP_DIRS': False, # 关闭去每个APP下查找,不设置默认值为False
        'OPTIONS': {
            # ... 同上 ...
        },
    },
]

若调用render(request,‘story_detail.html’),查找顺序如下,找到为止

# 1、先检索列表中的第一个引擎,即'django' 引擎
/home/html/example.com/story_detail.html 
/home/html/default/story_detail.html 

# 2、再检索列表中的第二个引擎,即'jinja2' 引擎
/home/html/jinja2/story_detail.html 

若调用render(request,[‘story_1_detail.html’, ‘story_detail.html’]),查找顺序如下,找到为止

# 一:首先查找模板story_1_detail.html
# 1.1、先检索列表中的第一个引擎,即'django' 引擎
/home/html/example.com/story_1_detail.html
/home/html/default/story_1_detail.html

# 1.2、再检索列表中的第二个引擎,即'jinja2' 引擎
/home/html/jinja2/story_1_detail.html 

# 二:其次查找模板story_detail.html
# 2.1、先检索列表中的第一个引擎,即'django' 引擎
/home/html/example.com/story_detail.html 
/home/html/default/story_detail.html 

# 2.2、再检索列表中的第二个引擎,即'jinja2' 引擎
/home/html/jinja2/story_detail.html 

所以针对[‘story_1_detail.html’, ‘story_detail.html’],后一个模板通常作为第一个模板的备胎,从而保障第一个模板在未找到时依然能够找到模板作为替代品,这样便可以使我们的代码更为灵活,如下

def article(request, article_id):
    first_template = 'article%s.html' % article_id
    bak_template = 'article.html'

    return render(request, [first_template, bak_template])

然后我们ARR_DIRS设置为True来讨论完整的加载顺序,需要事先在每个app目录下顶层建立目录templates(注意,目录名字必须为templates),

/Users/linhaifeng/PycharmProjects/EgonPro
├── app01
│ ├── templates *# 目录名必须为templates*
├── app02
│ ├── templates # 目录名必须为templates

然后配置如下

TEMPLATES = [
    {
        # django引擎
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            '/home/html/aaa',
            '/home/html/bbb',
        ],
        'APP_DIRS': True, # 关闭去每个APP下查找,不设置默认值为False
        'OPTIONS': {
            # ... 同上 ...
        },
    },
    {
        # jinja2引擎
        'BACKEND': 'django.template.backends.jinja2.Jinja2',
        'DIRS': [
            '/home/html/ccc',
        ],
        'APP_DIRS': True, # 关闭去每个APP下查找,不设置默认值为False
        'OPTIONS': {
            # ... 同上 ...
        },
    },
]

若调用render(request,‘story_detail.html’),查找顺序如下,找到为止

# 一:djanog会先依次检索每个引擎下的DIRS
# 1.1、先检索列表中的第一个引擎,即'django' 引擎
/home/html/aaa/story_detail.html 
/home/html/bbb/story_detail.html 

# 1.2、再检索列表中的第二个引擎,即'jinja2' 引擎
/home/html/ccc/story_detail.html 

# 二:因为APP_DIRS:True,所以接下来会按照INSTALLED_APPS中注册app的顺序,依次去每个app下的templates目录里查找
INSTALLED_APPS = [
    # ......
    'app02.apps.App02Config',
    'app01.apps.App01Config',
]

# 2.1 先检索app02下的tempaltes目录
/Users/linhaifeng/PycharmProjects/EgonPro/app02/templates/story_detail.html
# 2.2 再检索app01下的tempaltes目录
/Users/linhaifeng/PycharmProjects/EgonPro/app01/templates/story_detail.html

ps:针对render(request,[‘story_1_detail.html’, ‘story_detail.html’]),会按照列表中规定的模板顺序依次重复上述查找步骤,直到某一模板查找成功为止。

在一个项目的实际开发过程中,通常会包含多个app,多个app会有一些公共的模板文件,同时每个app也都会有自己对应的模板文件,要想清晰地组织这些模板,有如下两种解决方案

方案一:设置ARR_DIRS为False

#一:在项目根目录下的templates目录中新建子目录用来区分不同的模板归属
/Users/linhaifeng/PycharmProjects/EgonPro
├── templates
│   ├── base # 用于存放公共模板
│   │   ├── base.html
│   ├── app01 # 用于存放app01单独的模板
│   │   ├── index.html
│   ├── app02 # 用于存放app02单独的模板
│   │   ├── index.html

#二:配置如下
TEMPLATES = [
    # 引擎配置一
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  
        'APP_DIRS': False,
        'OPTIONS': {
            # ... 同上 ...
        },
    },
]

#三:渲染方式
render(request,'base/base.html')
render(request,'app01/index.html')
render(request,'app02/index.html')

方案二:设置ARR_DIRS为True

#一:在ARR_DIRS为True的情况下,会检索每个app的templates目录,所以需要事先创建好下述目录:
1、项目根目录/templates
2、项目根目录/app01/templates # 目录名字必须为templates
3、项目根目录/app02/templates # 目录名字必须为templates

注意:如果只创建到这一层就结束了,会出现冲突
比如render(request,'index.html'),在三个目录中都存在同名模板的情况下,查找优先级会是1,2,3(假设只有配置了一个引擎,且app的注册顺序为app01、app02)。
如果我们既想清晰地组织模板的目录结构,又想找到指定的模板、避免冲突,那么需要在1、2、3的基础上继续建立子目录,子目录名无所谓,但其发挥的作用,相当于名称空间了,如下
/Users/linhaifeng/PycharmProjects/EgonPro
├── templates
│   ├── base 
│   │   ├── base.html 
├── app02
│   ├── templates
│   │   └── app01
│   │       └── index.html 
├── app02
│   ├── templates
│   │   └── app02
│   │       └── index.html 

#二:配置如下
TEMPLATES = [
    # 引擎配置一
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  
        'APP_DIRS': True,
        'OPTIONS': {
            # ... 同上 ...
        },
    },
]

#三:渲染方式
# 最终找到的位置/templates/base/base.html
render(request,'base/base.html') 

# 最终找到的位置/app01/templates/app01/index.html
render(request,'app01/index.html')

# 最终找到的位置/app02/templates/app02/index.html
render(request,'app02/index.html')

(3)关键配置项目之NAME

在调用render()时,除非指定了引擎,否则会按照列表规定的顺序依次使用引擎来查找/加载模板,直到查找/加载成功为止。

如果想选用指定的模板引擎,需要使用参数NAME为每个模板引擎设定唯一的名字,然后在render中使用using参数指定即可

#一:settings.py配置如下
TEMPLATES = [
    # 引擎配置一
    {
        'NAME': 'b1',
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [os.path.join(BASE_DIR, 'aaa')],    
        'APP_DIRS': True,
        'OPTIONS': {
            # ... 同上 ...
        },
    },
    # 引擎配置二
    {
        'NAME': 'b2',
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [os.path.join(BASE_DIR, 'bbb')],    
        'APP_DIRS': True,
        'OPTIONS': {
            # ... 同上 ...
        },
    },
]

#二:为render函数指定using参数
render(request, 'index.html',using='b1') # b1->限定使用引擎配置一
render(request, 'index.html',using='b2') # b2->限定使用引擎配置二

ps:如果没有设定参数NAME,那么引擎的名字默认为BACKEND按照点为分隔符的倒数第二个值,例如

'django.template.backends.django.DjangoTemplates' 引擎名字为django
'django.template.backends.jinja2.Jinja2'          引擎名字为jinjia2

(4)关键配置项之OPTIONS

OPTIONS值为一字典,包含了要传递到模板后端的额外参数。比如

TEMPLATES = [
    {
        # ......
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
] 

我们在模板中常用的变量{{ request }}就是’django.template.context_processors.request’的功劳。

更多配置项请参照官网https://docs.djangoproject.com/en/3.0/topics/templates/#module-django.template.backends.django

三、DTL语法

Django模板指的是用Django模板语言(DTL)标记的文本文档(如HTML、XML、CSV等任何文本文档都可以)或者python字符串(文本文档也是由字符组成)。

Ps: DTL一定要在django的模板引擎下使用,不要使用jinja2引擎或者其他模板引擎

DTL的语法主要由四部分构成:变量、过滤器、标签、注释,如下

{% extends "base_generic.html" %}

{% block title %}{{ section.title }}{% endblock %}

{% block content %}
{#    <h1>hello</h1> #}
    <h1>{{ section.title }}</h1>

    {% for story in story_list %}
        <h2>
            <a href="{{ story.get_absolute_url }}">
                {{ story.headline|upper }}
            </a>
        </h2>
        <p>{{ story.tease|truncatewords:"100" }}</p>
    {% endfor %}
{% endblock %}

让我们来分别作详细介绍

1、DTL之变量

1.变量的基本使用

模板中的变量格式为:{{ 变量名 }}。变量名由字母数字和下划线组成,但是不能以下划线开头否则会因为引擎无法解析导致服务端错误。

如果模板中的数据不是固定死的,而是动态变化的,则必须在html中嵌入变量,如下

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p>{{ msg }}</p>
<p>{{ dic }}</p>
<p>{{ obj }}</p>
<p>{{ li }}</p>
</body>
</html>

我们需要在视图函数中为模板test.html的变量名msg、li、dic、obj、obj_li赋值,views.py内容如下

from django.shortcuts import render

def test(request):
    # 传给模板的变量值可以是任意python类型,如下
    msg='hello world'
    dic={'k1':1,'k2':2}
    class Person(object):
        def __init__(self,name,age):
            self.name=name
            self.age=age

    obj=Person('egon',18)
    li = [1,'aaa',obj]

    return render(request,'test.html',{'msg':msg,'dic':dic,'obj':obj,'li':li})
    # 注意:
    # 1、render函数的第三个参数称之为上下文数据,包含了要传给模板的变量值,是一个字典,该字典中的key必须与模板文件中的变量名相对应,render函数会去templates目录下找到模板文件,然后根据字典中的key对应到模板文件中的变量名进行赋值操作,最后将赋值后的模板文件内容返回给浏览器
    # 2、可以将render函数的第三个参数简写为locals(),如下
    return render(request,'test.html',locals()) #locals()会将函数test内定义的名字与值转换为字典中的k与v

2.深度查询之句点符的使用

变量名中不能有空格或者标点符号,但是有一个例外,点(".")可以出现在变量中,点后的可以是字典相关(字典的key或者字典内置方法)、对象的属性或方法、数字索引,如下所示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

{# 视图函数参照上例 #}

<!--调用字符串对象的upper方法,注意不要加括号-->
<p>{{ msg.upper }}</p>

<!--取字典中k1对应的值-->
<p>{{ dic.k1 }}</p>

<!--取对象的name属性-->
<p>{{ obj.name }}</p>

<!--取列表的第2个元素,然后变成大写-->
<p>{{ li.1.upper }}</p>

<!--取列表的第3个元素,并取该元素的age属性-->
<p>{{ li.2.age }}</p>

</body>
</html>

请注意四点

(1)在渲染页面时,若变量不存在,模板引擎默认用配置项string_if_invalid的值作为替代品,配置如下,默认值为空字符串”,可以进行配置

# 1、setting.py配置如下
TEMPLATES = [
    # 引擎配置一
    {
        'NAME': 'b1',
        'BACKEND': 'django.template.backends.django.DjangoTemplates', 
        'DIRS': [os.path.join(BASE_DIR, 'aaa')],    
        'APP_DIRS': True,
        'OPTIONS': {
            'string_if_invalid':'egon是大帅比',

            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# 2、渲染时针对不存在的变量,值都会被替换.

(2)当点后的值是可调用对象时,模板引擎在渲染时会当做无参函数进行调用,将调用的返回值插入模板。(Django不推荐我们在模板中做过多的逻辑处理,所以模板中的方法调全部为无参调用,即便是遇到有参的需求,那也应该是在后端处理,然后为模板提供处理结果即可)

(3)约定俗成:以下划线开头的变量属性通常被视为私有属性,可能无法访问

例如在视图函数中的变量是下划线开头的
_x = 222
在模板中是无法引用的
{{ _x }} 报错*
*
ps:__x也属于下划线开头的

(4)点后值的查找优先级如下

1、先进行: 字典key的查找
2、然后进行:属性或方法的查找
3、最后进行:数字索引的查找

针对上述优先级,需要尤其注意冲突问题,例如

def test(request):
    from collections import defaultdict
    msg='helloegon'
    d=defaultdict(int)
    for s in msg:
        d[s]+=1

    return render(request, 'test.html', {'d':d})

针对defaultdict字典,在访问某一个不存在的key时会返回一个默认值,这就为冲突埋下了祸根:

在模板中插入{{ d.items }},我们的本意是想调用字典d的方法items(),但实际情况是会首先查找字典的key,所以items会被首先当做key来进行查找,很明显字典d中不存在名为items的key,而针对defaultdict类型的字典,会在key不存在时返回一个默认值(本例返回默认值为0),所以此时并不会调用items()方法。

针对上述问题的解决方案就是需要在视图中事先将defaultdict转换为dict类型

d=dict(d)

然后再传递给模板,这样在渲染{{ d.items }}时,先去字典中查找键items,无法找到也不会返回任何默认值,所以会转而查找d的属性或方法,从而调用到方法items()

2、DTL之过滤器

1.常用过滤器基本使用

过滤器类似于python的内置函数,用来把变量值加以修饰后再显示,具体语法如下

#1、
{{ 变量名|过滤器名 }}

#2、链式调用:上一个过滤器的结果继续被下一个过滤器处理
{{ 变量名|过滤器1|过滤器2 }}

#3、有的过滤器取需要参数
{{ 变量名|过滤器名:传给过滤器的参数 }}

常用内置过滤器

#0、default
#作用:如果一个变量值是False或者为空、None,使用default后指定的默认值,否则,使用变量本身的值,如果value=’‘则输出“nothing”
{{ value|default:"nothing" }}

#1、default_if_none
#作用:如果只针对value是None这一种情况来设置默认值,需要使用default_if_none
#只有在value=None的情况下,才会输出“None...”,
{{ value|default_if_none:"None..." }}

#2、length
#作用:返回值的长度。它对字符串、列表、字典等容器类型都起作用,如果value是 ['a', 'b', 'c', 'd'],那么输出是4
{{ value|length }}

#3、filesizeformat
#作用:将值的格式化为一个"人类可读的"文件尺寸(如13KB、4.1 MB、102bytes等等),如果 value 是 12312312321,输出将会是 11.5 GB
{{ value|filesizeformat }}

#4、date
#作用:将日期按照指定的格式输出,如果value=datetime.datetime.now(),按照格式Y-m-d则输出2019-02-02
{{ value|date:"Y-m-d" }}  

#5、slice
#作用:对输出的字符串进行切片操作,顾头不顾尾,如果value=“egon“,则输出"eg"
{{ value|slice:"0:2" }} 

#6、truncatechars
#作用:如果字符串字符多于指定的字符数量,那么会被截断。截断的字符串将以可翻译的省略号序列(“...”)结尾,如果value=”hello world egon 嘎嘎“,则输出"hello...",注意8个字符也包含末尾的3个点
{{ value|truncatechars:8 }}

#7、truncatewords
#作用:同truncatechars,但truncatewords是按照单词截断,注意末尾的3个点不算作单词,如果value=”hello world egon 嘎嘎“,则输出"hello world ..."
{{ value|truncatewords:2 }}

2. HTML的自动转义与关闭

模板在生成HTML时,如果变量中包含一些具有语法意义的特殊字符,则会影响HTML的结果,比如

Hello, {{ name }}

针对变量{{ name }},如果用户注册自己的用户名name是一个中规中矩的用户名时,上述代码并无问题,但如果用户恶意注册用户名为下述内容

# 注册时,用户输入自己的用户名就是下述字符
<script>alert('hello')</script>

后台取出name = "alert(‘hello’)",然后执行render渲染的结果为

Hello, <script>alert('hello')</script>

上述结果交给浏览器后,意味着浏览器将弹出一个JavaScript警报框!试想,如果是一个博客类网站,恶意作者在自己提交的文章中掺杂了类似上面这种恶意代码,这意味着每个读者在读取他的文章时,自己的浏览器都会弹出一个JavaScript警报框!

类似的,如果注册的用户名包含'<‘号,如下

<b>egon

后台取出name = "egon",然后执行render渲染的结果为

Hello, <b>egon

上述结果交给浏览器后,有没有对应的闭合标签,意味着其后的内容都会被加粗

综上,可以确定的是:用户提交的数据不应该被盲目地信任并直接插入到我们的的网页中,因为恶意用户可能会利用这种漏洞来做潜在的坏事。这种类型的安全攻击称为跨站点脚本(XSS,详见:https://en.wikipedia.org/wiki/Cross-site_scripting)攻击。

好在django已经为了做了相应的处理:django的模板引擎在生成模板时,默认就会对所有变量的值进行转移,具体是针对变量值中包含的以下五个字符的转义

# 1、使用DTL,以下5种特殊符号默认就会被转义成对应的html命名实体
1、< 被转换成 <
2、> 被转换成 >
3、' 单引号被转换成 &#x27;
4、" 双引号被转换成 "
5、& 被转换成 &

# 首先经过转义后得到模板,然后递交给浏览器解析,上述内容均会被当成普通字符输出
例如:
针对value="<script>alert(123)</script>",模板变量{{ value }}会被渲染成<script>alert(123)</script>交给浏览器后会被解析成普通字符”<script>alert(123)</script>“,失去了js代码的语法意义

虽然django默认会将所有模板变量进行转义,但有时候我们需要关闭自动转义,比如我们存入数据库的就是一段HTML代码,当我们取出来时就想让浏览器解析其中的语法呈现结果,而不是显示一堆普通字符。那如何关闭自动转义呢?具体操作如下

(1)针对单个变量->使用过滤器safe

{{ value|safe }}

比如如value='<a href="https://www.baidu.com">点我啊</a>',经过过滤器safe的处理,浏览器在进行解析时就会将其当做超链接显示,不加safe过滤器则会当做普通字符显示’<a href="https://www.baidu.com">点我啊</a>‘

(2)针对模板块->使用标签autoescape

autoescape通过参数on和off来控制开启或关闭模板块的整体转义行为,示例如下

{# 对包含在标签内的模板块的转义行为进行整体关闭 #}
{% autoescape off %}
    <p>
        不会转义 {{ name }}.
    </p>

    {#  支持嵌套,设置嵌套的模板块整体开启转义功能 #}
    {% autoescape on %}
        <p>
            会被转义: {{ name }}
        </p>
    {% endautoescape %}
{% endautoescape %}

autoescape的效果会遗传给子模板(使用标签extends继承当前模板),也会留给引入了当前模板的模板(使用标签include引入当前模板),例如

base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{% autoescape on %}
    <h1>
        {% block title %}{% endblock %}
    </h1>

    {% block content %}{% endblock %}
{% endautoescape %}

</body>
</html>

child.html

{# 继承父模板base.html #}
{% extends "base.html" %}

{# 定制title块中的内容,其中包含的内容没有特殊字符,转义是否开启没有影响,显示结果均为egon & dsb #}
{% block title %}egon & dsb{% endblock %}

{# 定制content块中的内容,如果变量name="<b>egon</b>",由于content模板块在父模板中被设置成关闭转义,所以子模板也跟着关闭,生成的值为<b>egon</b>,交给浏览器后显示结果为加粗的egon  #}
{% block content %}{{ name }}{% endblock %}

ps: autoescape的使用涉及到的标签与模板继承的概念,请读者自行查看后续章节

我们在介绍常用的内置过滤器时提到过:过滤器的参数可以是字符串,比如default过滤器,而default过滤器默认不会对字符串进行转义

{{ value1|default:"<a href='http://www.egonav.com'>egon草裙舞</a>" }}

即便是我们用标签{% autoescape on %}开启了自动转义,过滤器default仍然不会对字符串进行转义,所以上述代码在value值为Flase/空/None的情况下,浏览器展示的就是一个超链接,如果我们仅仅只想把defaul的值显示成字符串,那么需要我们手动把默认值里的特殊符号写成命名实体,如下

{{ value1|default:"<a href=&#x27;http://www.egonav.com&#x27;>egon草裙舞</a>" }}

当我们要显示的默认值是“3 < 2”时,因为小于号<与数值2并不会组成一个特殊标签,所以浏览器会显示出“3 < 2”

{{ data|default:"3 < 2" }}  {# Bad! Don't do this. #}

但是我们还是应该写成下面这种格式,这才是通用的书写格式

{{ data|default:"3 < 2" }}

3.其他过滤器(了解)

详见官网:https://docs.djangoproject.com/en/3.0/ref/templates/builtins/#ref-templates-builtins-filters

过滤器 描述 示例
upper 以大写方式输出 {{ user.name | upper }}
add 给value加上一个数值 {{ user.age | add:”5” }}
addslashes 单引号加上转义号
capfirst 第一个字母大写 {{ ‘good’| capfirst }} 返回”Good”
center 输出指定长度的字符串,把变量居中 {{ “abcd”| center:”50” }}
cut 删除指定字符串 {{ “You are not a Englishman” | cut:”not” }}
date 格式化日期
default_if_none 如果值为None, 则使用默认值代替
dictsort 按某字段排序,变量必须是一个dictionary {% for moment in moments | dictsort:”id” %}
dictsortreversed 按某字段倒序排序,变量必须是dictionary
divisibleby 判断是否可以被数字整除 {{ 224 | divisibleby:2 }} 返回 True
escape 按HTML转义,比如将”<”转换为”<”
filesizeformat 增加数字的可读性,转换结果为13KB,89MB,3Bytes等 {{ 1024 | filesizeformat }} 返回 1.0KB
first 返回列表的第1个元素,变量必须是一个列表
floatformat 转换为指定精度的小数,默认保留1位小数 {{ 3.1415926 | floatformat:3 }} 返回 3.142 四舍五入
get_digit 从个位数开始截取指定位置的数字 {{ 123456 | get_digit:’1’}}
join 用指定分隔符连接列表 {{ [‘abc’,’45’] | join:’’ }} 返回 abc45
length 返回列表中元素的个数或字符串长度
length_is 检查列表,字符串长度是否符合指定的值 {{ ‘hello’| length_is:’3’ }}
linebreaks 用或 标签包裹变量 {{ “Hi\n\nDavid”|linebreaks }} 返回HiDavid
linebreaksbr 用 标签代替换行符
linenumbers 为变量中的每一行加上行号
ljust 输出指定长度的字符串,变量左对齐 {{‘ab’|ljust:5}}返回 ‘ab ’
lower 字符串变小写
make_list 将字符串转换为列表
pluralize 根据数字确定是否输出英文复数符号
random 返回列表的随机一项
removetags 删除字符串中指定的HTML标记 {{value | removetags: “h1 h2”}}
rjust 输出指定长度的字符串,变量右对齐
slice 切片操作, 返回列表 {{[3,9,1] | slice:’:2’}} 返回 [3,9] {{ ‘asdikfjhihgie’ | slice:’:5′ }} 返回 ‘asdik’
slugify 在字符串中留下减号和下划线,其它符号删除,空格用减号替换 {{ ‘5-2=3and5 2=3’ | slugify }} 返回 5-23and5-23
stringformat 字符串格式化,语法同python
time 返回日期的时间部分
timesince 以“到现在为止过了多长时间”显示时间变量 结果可能为 45days, 3 hours
timeuntil 以“从现在开始到时间变量”还有多长时间显示时间变量
title 每个单词首字母大写
truncatewords 将字符串转换为省略表达方式 {{ ‘This is a pen’ | truncatewords:2 }}返回“This is …
truncatewords_html 同上,但保留其中的HTML标签 {{ ‘

This is a pen

‘ | truncatewords:2 }}返回“

This is …

urlencode 将字符串中的特殊字符转换为url兼容表达方式 {{ ‘http://www.aaa.com/foo?a=b&b=c’ | urlencode}}
urlize 将变量字符串中的url由纯文本变为链接
wordcount 返回变量字符串中的单词数
yesno 将布尔变量转换为字符串yes, no 或maybe {{ True | yesno }}{{ False | yesno }}{{ None | yesno }} 返回 yesno maybe

3、DTL之标签

相比于模板中的变量,标签更为复杂一些,如

#1、一些标签用来在输出中创建文本
#2、一些标签用来进行流程控制(执行循环或者if判断)
#3、一些标签用来将外部信息加载到模板中以供以后的变量使用

模板中的标签的格式为

# 1、
{% 标签名 %}

# 2、大多数标签都需要接收参数
{% 标签名 参数1 参数2 %}

# 3、一些标签需要有开始{% tag %}和结束标记{% endtag %}
{% 标签名 %}
...内容...
{% end标签名 %}

1.常用标签之for标签

#1、遍历每一个元素:
{% for person in person_list %}
    <p>{{ person.name }}</p>
{% endfor %}

#2、可以利用{% for obj in list reversed %}反向循环。

#3、遍历一个字典:
{% for key,val in dic.items %}
    <p>{{ key }}:{{ val }}</p>
{% endfor %}

#4、循环序号可以通过{{ forloop }}显示 
forloop.counter            当前循环的索引值(从1开始)
forloop.counter0           当前循环的索引值(从0开始)
forloop.revcounter         当前循环的倒序索引值(从1开始)
forloop.revcounter0        当前循环的倒序索引值(从0开始)
forloop.first              当前循环是第一次循环则返回True,否则返回False
forloop.last               当前循环是最后一次循环则返回True,否则返回False
forloop.parentloop         本层循环的外层循环

#5、for标签可以带有一个可选的{% empty %} 从句,在变量person_list为空或者没有被找到时,则执行empty子句
{% for person in person_list %}
    <p>{{ person.name }}</p>

{% empty %}
    <p>sorry,no person here</p>
{% endfor %}

了解:Django框架的for循环,没有break和continue方法,可以使用自定义过滤器实现forloop | continue和forloop | break,参考:https://djangosnippets.org/snippets/2093/

案列如下

url.py

from django.urls import re_path
from app01 import views

urlpatterns = [
    re_path(r'^test/',views.test)
]

view.py

def test(request):
    names=['egon','kevin']
    dic={'name':'egon','age':18,'sex':'male'}

    list1=[]

    return render(request,'test.html',locals())

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<hr>
{% for name in names %}
    <p>{{ forloop.counter0 }} {{ name }}</p>
{% endfor %}
<!--
输出结果为:
0 egon
1 kevin
-->

<hr>
{% for name in names reversed %}
    <p>{{ forloop.revcounter0 }} {{ name }}</p>
{% endfor %}
<!--
输出结果为:
1 kevin
0 egon
-->

<hr>
{% for k,v in dic.items %}
    <p>{{ forloop.counter }} {{ k }} {{ v }}</p>
{% endfor %}
<!--
输出结果为:
1 name egon
2 age 18
3 sex male
-->

<hr>
{% for item in list1 %}
    <p>{{ item }}</p>
    {% empty %}
        <p>sorry,no value here</p>
{% endfor %}
<!--
输出结果为:
sorry,no value here
-->

</body>
</html>

2.常用标签之if标签

# 1、注意:
{% if 条件 %}条件为真时if的子句才会生效,条件也可以是一个变量,if会对变量进行求值,在变量值为空、或者视图没有为其传值的情况下均为False

# 2、具体语法
{% if num > 100 or num < 0 %}
    <p>无效</p>
{% elif num > 80 and num < 100 %}
    <p>优秀</p>
{% else %}
    <p>凑活吧</p>
{% endif %}

#3、if语句支持 and 、or、==、>、<、!=、<=、>=、in、not in、is、is not判断。

#4、判断条件中可以引入过滤器
{% if athlete_list|length > 1 %}
   Team: {% for athlete in athlete_list %} ... {% endfor %}
{% else %}
   Athlete: {{ athlete_list.0.name }}
{% endif %}

过滤器length返回的数字可用于与数字进行比较,除此之外大多数过滤器返回的都是字符串并不能用于与数字比较

#5、补充标签firstof
针对下述多分支
    {% if var1 %}
        {{ var1 }}
    {% elif var2 %}
        {{ var2 }}
    {% elif var3 %}
        {{ var3 }}
    {% endif %}

可以简写为一行
    {% firstof var1 var2 var3 %}

也可以定义一个备用值,当var1、var2、var3均无值的时使用
{% firstof var1 var2 var3 "fallback value" %}

案例如下

urls.py

from django.urls import path,register_converter,re_path
from app01 import views

urlpatterns = [
    # 输入http://127.0.0.1:8008/或者http://127.0.0.1:8008/index/都会转发给视图函数index
    re_path(r'^$',views.index),
    re_path(r'^index/$',views.index),

    re_path(r'^login/',views.login),

]

views.py

from django.shortcuts import render

def index(request):
    return render(request,'index.html')

def login(request):
    if request.method == 'GET':
        return render(request,'login.html')

    name=request.POST.get('name')
    pwd=request.POST.get('pwd')
    if name == 'egon' and pwd == '123':
        current_user=name
        return render(request,'index.html',locals())
    else:
        msg='账号或密码错误'
        return render(request,'login.html',locals())

在templates目录下新建模板文件index.html与login.html

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>

<h3>首页</h3>
<!--
如果用户已经登录,则current_user变量有值,if判断结果为真,会打印变量current_user的值,为当前登录的用户名
如果用户没有登录,则current_user变量无值,if判断结果为假,会打印a标签要求用户先登录
-->
{% if current_user %}
    <p>当前登录用户为:{{ current_user }}</p>
{% else %}
    <p><a href="/login/">请先登录</a></p>
{% endif %}

</body>
</html>

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>

<form action="" method="POST">
    {% csrf_token %}
    <p>用户名:<input type="text" name="name"></p>
    <p>密码:<input type="password" name="pwd"></p>
    <p><input type="submit" value="提交"></p>
</form>
<!--输错账号密码时的提示信息-->
<p style="color: red">{{ msg }}</p>
</body>
</html>

测试

python manage.py runserver 8008 #在浏览器输入http://127.0.0.1:8008/,然后点击登录,输入账号密码进行验证......

3.常用标签之with标签

# with标签用来为一个复杂的变量名起别名,如果变量的值来自于数据库,在起别名后只需要使用别名即可,无需每次都向数据库发送请求来重新获取变量的值
{% with li.1.upper as v %}
    {{ v }}
{% endwith %}

4.常用标签之csrf_token标签

# 当用form表单提交POST请求时必须加上标签{% csrf_token%},该标签用于防止跨站伪造请求
<form action="" method="POST">
    {% csrf_token %}
    <p>用户名:<input type="text" name="name"></p>
    <p>密码:<input type="password" name="pwd"></p>
    <p><input type="submit" value="提交"></p>
</form>
# 具体工作原理为:
# 1、在GET请求到form表单时,标签{% csrf_token%}会被渲染成一个隐藏的input标签,该标签包含了由服务端生成的一串随机字符串,如<input type="hidden" name="csrfmiddlewaretoken" value="dmje28mFo...OvnZ5">
# 2、在使用form表单提交POST请求时,会提交上述随机字符串,服务端在接收到该POST请求时会对比该随机字符串,对比成功则处理该POST请求,否则拒绝,以此来确定客户端的身份

# ps:什么是跨站请求伪造(英语:Cross-site request forgery)详见附录2

更多内置标签与过滤器:https://docs.djangoproject.com/en/3.0/ref/templates/builtins/#ref-templates-builtins-tags

4、DTL之注释

单行注释,用注释语法:{# #},在{#和#}分隔符之间不允许有换行符

#1、注释一行的某一部分,如下,渲染出的结果只会显示hello
<h1>hello{# egon #}</h1>

#2、注释可以包含任何模板代码,如下
{# {% if foo %}bar{% else %} #}

多行注释,用comment标签,注意:comment标签不能嵌套

{% comment "可以在该引号里写好下述代码被注释的原因" %}
    <p>备注释的内容</p>
{% endcomment %}

四、模板的导入和继承

在实际开发中,模板文件彼此之间可能会有大量冗余代码,为此django提供了专门的语法来解决这个问题,主要围绕三种标签的使用:include标签、extends标签、block标签,详解如下

1、模板的导入之include标签

作用:在一个模板文件中,引入/重用另外一个模板文件的内容,

{% include ‘模版名称’ %}

案例:

可以把广告栏写到专门的文件里advertise.html

<div class="adv">
    <div class="panel panel-default">
        <div class="panel-heading">
            <h3 class="panel-title">Panel title</h3>
        </div>
        <div class="panel-body">
            Panel content
        </div>
    </div>
    <div class="panel panel-danger">
        <div class="panel-heading">
            <h3 class="panel-title">Panel title</h3>
        </div>
        <div class="panel-body">
            Panel content
        </div>
    </div>
    <div class="panel panel-warning">
        <div class="panel-heading">
            <h3 class="panel-title">Panel title</h3>
        </div>
        <div class="panel-body">
            Panel content
        </div>
    </div>
</div>

然后在base.html文件中用include标签引入advertise.html文件的内容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .header {
            height: 50px;
            width: 100%;
            background-color: black;
        }

    </style>
</head>
<body>
<div class="header"></div>
<div class="container">
    <div class="row">
        <div class="col-md-3">
            <!--在base.html引入advertise.html文件的内容-->
            {% include "advertise.html" %}
        </div>
        <div class="col-md-9"></div>
    </div>
</div>
</body>
</html>

2、模板的继承\派生之extends标签、block标签

#作用:在一个模板文件中,引入/重用另外一个模板文件的内容
{% extends "模版名称" %}
#  也就是说include有的功能extends全都有,但是extends可以搭配一个block标签,用于在继承的基础上定制新的内容

案例

Django模版引擎中最复杂且最强大的部分就是模版继承了。我们以先创建一个基本的“骨架”模版,它包含我们站点中的全部元素,并且可以定义多处blocks ,例如我们创建base.html内容如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>
        {% block title %}自定义title名{% endblock %}
    </title>

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .header {
            height: 50px;
            width: 100%;
            background-color: #919191;
            margin-bottom: 20px;
        }

    </style>

</head>
<body>
<div class="header"></div>

<div class="container">
    <div class="row">
        <div class="col-md-3">
            <div class="list-group">
                {% block sidebar %}
                    <a href="#" class="list-group-item active">服装城</a>
                    <a href="#" class="list-group-item">美妆馆</a>
                    <a href="#" class="list-group-item">超市</a>
                    <a href="#" class="list-group-item">全球购</a>
                    <a href="#" class="list-group-item">闪购</a>
                    <a href="#" class="list-group-item">团购</a>
                {% endblock %}

            </div>
        </div>

        <div class="col-md-9">
            {% block content %}
                base.html页面内容
            {% endblock %}
        </div>
    </div>

</div>

</body>
</html>

模板base.html 定义了一个可以用于两列排版页面的简单HTML骨架。我们新建子模板index.html的主要工作就是继承base.html然后填充/覆盖其内部的blocks。

{% extends "base.html" %}

<!--用新内容完全覆盖了父模板内容-->
{% block title %}
    index页面
{% endblock %}

{% block sidebar %}
    <!--该变量会将父模板中sidebar中原来的内容继承过来,然后我们可以在此基础上新增,否则就是纯粹地覆盖-->
    {{ block.super }}

    <!--在继承父模板内容的基础上新增的标签-->
    <a href="#" class="list-group-item">拍卖</a>
    <a href="#" class="list-group-item">金融</a>
{% endblock %}

{% block content %}
    <!--用新内容完全覆盖了父模板内容-->
    <p>index页面内容</p>
{% endblock %}

我们通过django访问index.html看到内容如下(block标签的内容都完成了替换或更新)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>
        index页面
    </title>

    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .header {
            height: 50px;
            width: 100%;
            background-color: #919191;
            margin-bottom: 20px;
        }

    </style>

</head>
<body>
<div class="header"></div>

<div class="container">
    <div class="row">
        <div class="col-md-3">
            <div class="list-group">
                <!--基于{{ block.super }}继承自父模板中的内容-->
                <a href="#" class="list-group-item active">服装城</a>
                <a href="#" class="list-group-item">美妆馆</a>
                <a href="#" class="list-group-item">超市</a>
                <a href="#" class="list-group-item">全球购</a>
                <a href="#" class="list-group-item">闪购</a>
                <a href="#" class="list-group-item">团购</a>

                <!--在继承父模板内容的基础上新增的标签-->
                <a href="#" class="list-group-item">拍卖</a>
                <a href="#" class="list-group-item">金融</a>
            </div>
        </div>

        <div class="col-md-9">
            <!--用新内容完全覆盖了父模板内容-->
            <p>index页面内容</p>
        </div>
    </div>

</div>

</body>
</html>

我们可以根据需要使用尽可能多的继承级别,使用继承的一种常见方式是分成三个级别

1、创建一个base.html用来保存站点的主要外观
2、为站点的每个部分创建一个base_SECTIONNAME.html模板。例如,base_news.html、base_sports.html。这些模板都继承自base.html并包含每部分特定的样式/设计。
3、为每种类型的页面创建单独的模板,如新闻文章或博客条目。这些继承自2中的对应的模板。

总结与注意:

#1、include仅仅只是完全引用其他模板文件,而extends却可以搭配block在引用的基础上进行扩写

#2、如果使用模板继承,那么子模板必须将标签{% extends %}放在首行,否则模板继承无效。

#3、base.html中{% block %}标记越多,它的可定制性就越强。子模板不必将父模板中定义的{% block %}全部重新定义一遍,你只需要定义自己需要的即可,未定义的{% block %}会继承父模板的内容

#4、如果你发现在多个模板中存在重复的内容,我们应该将其放入父模板中

#5、变量{{ block.super }} 可以重用父类的内容,然后在父类基础上增加新内容,而不是完全覆盖。使用{{block.super}插入的数据将不会自动转义,因为它已在父模板中转义(如果需要)。

#6、为了提升可读性,我们可以给标签{% endblock %} 起一个名字 。例如:
    {% block content %}
    ...
    {% endblock content %} 

#7、在一个模版中不能出现重名的block标签。

五、自定义过滤器和标签

1、自定义过滤器和标签

当内置的过滤器或标签无法满足我们需求时,我们可以自定义,具体操作步骤如下

1、在settings中的INSTALLED_APPS添加当前app的名字,不然django无法找到自定义的过滤器或标签

settings.py

# 在settings.py中找到该列表,然后加以配置
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01.apps.App01Config',
    'app01', # 添加当前app的名字
]

2、在文件夹app01下创建包templatetags

1、templatetags目录下需要有init.py文件,确保它是一个python的包
2、文件夹名只能是templatetags
3、templatetags与models.py,views.py在同一级别

3、在templatetags新建任意.py文件,如my_tags.py,在该文件中自定义过滤器或标签,由于我们后期是要通过加载文件来引入自定义的过滤器或标签,所以请保障文件名的唯一性,如此方可保障多个app之间不相互冲突

目录层级如下

app01/
    __init__.py
    models.py
    templatetags/
        __init__.py
        my_tags.py
    views.py

# 加载方式:{% load 自定义标签或过滤器所处的文件名字 %},本例对应为{% load my_tags %}

my_tags.py文件内容如下

#一、必须首先在模板顶级定义一个变量register,该变量值为template.Library的实例
from django import template

register = template.Library() # 注意变量名必须为register,不可改变

#二、然后再开始自定义过滤器与标签
'''
2.1、自定义过滤器
    自定义过滤器是一个python函数,该函数需要1或2个参数,两个参数会一同传入
    (1) 第一个参数来自过滤器左侧的变量(固定有)
    (2) 第二个参数来自过滤右侧的参数值(可有可无)
    例如:针对自定义过滤器foo的使用
    {{ var|foo:"bar" }},会执行foo(var,"bar")
'''
@register.filter(name='cut') # name参数用来命名自定义过滤器
def cut(value, arg): # 有两个参数
    """Removes all values of arg from the given string"""
    return value.replace(arg, '')

@register.filter # 省略name参数,过滤器名字默认为函数名
def lower(value): # 只有一个参数
    """Converts a string into all lowercase"""
    return value.lower()

#2.2、自定义标签:可以定义多个参数
@register.simple_tag
def my_multi_tag(v1, v2): 
    return v1 * v2

'''
2.3、自定义标签扩展之mark_safe
    我们可以用内置标签safe来关闭转义,从而让标签内容有语法意义,但如果我们想让自定义标签处理的结果也有语法意义,则不能使用内置标签safe了,需要使用mark_safe,可以实现与内置标签safe同样的功能
'''
from django.utils.safestring import mark_safe

@register.simple_tag
def my_input_tag(id, name):
    res = "<input type='text' id='%s' name='%s' />" % (id, name)
    return mark_safe(res)

4、自定义过滤器或标签必须重新启动django方可生效

2、自定义过滤器和标签的使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<!--必须先加载存有自定义过滤器和标签的文件-->
{% load my_tags %}

<!--过滤器cut使用:处理结果为:Egon is 18 years old-->
{{ 'Egon is 180 years old'|cut:"0" }}

<!--过滤器lower使用:处理结果为:egon-->
{{ 'Egon'|lower }}

<!--结果为2-->
{% my_multi_tag 1 2 %}

<!--
结果为一个input标签,该表的属性id="inp1" name="username"
注意:input的属性值均为字符串类型,所以my_input_tag后的两个值均为字符串类型
-->
{% my_input_tag "inp1" "username" %} 

</body>
</html>

对比自定义标签与自定义过滤器

#1、自定义过滤器只能传两个参数,而自定义标签却可以传多个参数

#2、过滤器可以用于if判断,而标签不能
{% if salary|my_multi_filter:12 > 200 %}
    <p>优秀</p>
{% else %}
    <p>垃圾</p>
{% endif %}

3、Inclusion tags

在4.1小节我们提及内置的include标签可以在当前模板中引入其他的模板,但有一个问题是:被引入的模板内容是固定死的,如果我们想根据传入不同的参数控制被引入模板的内容,就需要自定义inclusion标签。

先创建好被引入的模板userinfo.html,嵌入变量

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>靓仔的名字: {{ username }}</h1>
<h1>靓仔的年龄: {{ age }}</h1>
</body>
</html>

然后自定义inclusion标签

# my_tags.py,其余准备工作请读者自行参考自定义标签,此处我们直接上代码
from django import template

register = template.Library()

@register.inclusion_tag('userinfo.html')
def get_userinfo(username, age):
    '''
    可以函数内编写一系列的逻辑,比如根据传来的参数,去后台数据库中取出相应的数据
    '''

    res = {
        'username': username,
        'age': age,
    }
    return res

自定义标签get_userinfo的返回值必须是一个字典,该字典用来渲染模板userinfo.html。

如果我们想在index.html中引入userinfo.html的内容,做法如下,为标签get_userinfo传入不同的参数则会得到不同的模板内容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{% load my_tags %}
{% get_userinfo "egon" age=18 %}  {#等号左右两边不能有空格#}
</body>
</html>

自定义inclusion标签函数可以接收任意数量的位置或关键字参数,例如

@register.inclusion_tag('my_template.html')
def my_inclusion_tag(a, b, *args, **kwargs):
    warning = kwargs['warning']
    profile = kwargs['profile']
    ...
    return 用于渲染模板'my_template.html'的字典

但此时的参数都需要在模板中直接传递,比如

{% my_inclusion_tag "egon" 18 book.title warning=message|lower profile=user.profile %}

毫无疑问,让模板的开发者去记住所有参数的顺序并进行传递是非常痛苦的,为此register.inclusion_tag提供了一个参数takes_context专门用来解决这个问题。该参数默认值为False,当设置成True时,就像单词字面所表达的一样:从contex中获取参数。context是由当前模板对应的视图返回的,如当前模板是index.html,那context就来自于index.html对应的后台视图返回。所以当takes_context=True时,自定义标签只需有一个参数,且参数名必须为context,与render函数的context参数相呼应

my_template.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{{ aaa }}
{{ bbb }}
{{ ccc }}
{{ ddd }}
</body>
</html>

my_tags.py

from django import template

register = template.Library()  # 注意变量名必须为register,不可改变

@register.inclusion_tag('my_template.html', takes_context=True)
def my_inclusion_tag(context):

    res = {
        'aaa': context['a'],
        'bbb': context['b'],
        'ccc': context['warning'],
        'ddd': context['profile'],
        'other_data':'...'
    }
    return res # 该字典是用来渲染模板'my_template.html'的,不要与context混淆

如果我们想在index.html中引入my_template.html的内容,做法如下,此时无需为标签my_inclusion_tag传入参数

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{% load my_tags %}
{% my_inclusion_tag %}
</body>
</html>

my_inclusion_tag的参数是来自于当前模板(即index.html)对应的后台视图返回的context

from django.shortcuts import render

def index(request):
    # 该字典与标签my_inclusion_tag的参数context呼应
    context = {
        'a': "egon",
        'b': 18,
        'warning': 'xxx',
        'profile': 'yyy'
    }
    return render(request, 'index.html', context=context)

六、静态文件配置

1、单app下静态文件的组织与使用

我们在编写模板文件时,需要大量引用css、js、图片等静态文件,如果我们将这些文件在服务端存放的路径都固定写死那么将非常不利于后期的扩展,我们可以这么做

1、settings.py

STATIC_URL = '/static/' # 找到这一行,然后新增下述代码
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'statics'),# 获取静态文件在服务端的绝对路径
]
#STATIC_URL = '/static/'就是为静态文件的绝对路径起了一个别名,以后我们只需要用路径/static/即可

2、在项目根目录下新增文件夹statics,为了更便于管理,可以在statics下新建子文件夹css、js、img等

插图:statics目录结构

3、新建模板文件index.html,在该文件中对静态文件的引用如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="/static/css/my.css">
</head>
<body>
<h4>我是红色的,点我就绿</h4>
<img src="/static/img/rb.jpeg" alt="">

<script src="/static/js/jquery-3.3.1.min.js"></script>
<script src="/static/js/my.js"></script>

</body>
</html>

综上,在配置完settings.py后,所有的静态文件路径都可以采用别名/static/作为起始,这在一定程度上会有利于程序的扩展性,但如果我们在项目后期维护时,连/static/这个值也需要修改,那意味着所有模板文件中也都需要跟着改了,扩展性依然很差,为此,django在一个名为static.py的文件中定义了标签static、get_static_prefix,二者都可以解决该问题

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--注意:必须先加载文件static.py-->
    {% load static %}
    <!--注意:此处的static是一个定义在static.py中的一个标签,名字与文件名一样而已,不要搞混-->
    <link rel="stylesheet" href="{% static 'css/my.css' %}">
</head>
<body>
<h4>我是红色的,点我就绿</h4>
<img src="{% static 'img/rb.jpeg' %}" alt="">

{% load static %}
<script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
<script src="{% static 'js/my.js' %}"></script>

</body>
</html>

标签static会接收传入的参数,然后这根据settings.py中变量STATIC_URL的值拼接出一个完整的路径,如果STATIC_URL = ‘/static/’,那么href="{% static ‘css/my.css’ %}"会被渲染成href="/static/css/my.css",如果STATIC_URL = ‘/static123/’,那么href="{% static ‘css/my.css’ %}"会被渲染成href="/static123/css/my.css"。

标签get_static_prefix也可以完成同样的效果,只不过用法不同。我们不能为标签get_static_prefix传参,因为标签get_static_prefix代表的只是settings.py中STATIC_URL的值,所以我们需要做的是在get_static_prefix的基础上自行拼接路径,如下

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--注意:同样需要先加载文件static.py-->
    {% load static %}
    <!--使用标签get_static_prefix拼接路径-->
    <link rel="stylesheet" href="{% get_static_prefix %}css/my.css">
</head>
<body>
<h4>我是红色的,点我就绿</h4>
<img src="{% get_static_prefix %}img/rb.jpeg" alt="">

{% load static %}
<script src="{% get_static_prefix %}js/jquery-3.3.1.min.js"></script>
<script src="{% get_static_prefix %}js/my.js"></script>

</body>
</html>

如果STATIC_URL = ‘/static/’,那么href="{% get_static_prefix %}css/my.css"会被渲染成href="/static/css/my.css",其它同理

2、多app下静态文件的组织与使用

静态文件的查找与模板的查找是一致的,当我们有多个app时,可以如下组织目录结构(只列举了静态文件相关目录)

/Users/linhaifeng/PycharmProjects/EgonPro
├── app01
│ ├── static
│ │ └── a.css *# 文件内容为:h1 { color: red; }
├── app02
│ ├── static
│ │ └── a.css # 文件内容为:h1 { color: green; }

├── static
│ └── a.css # 文件内容为:h1 { color: blue; }
├── static1
│ └── a.css # 文件内容为:h1 { color: goldenrod; }

├── templates
*│ └── index.html

配置文件settings.py

STATIC_URL = '/static/'

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

在模板index.html中,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    {% load static %}

    <link rel="stylesheet" href="{% static "a.css" %}">
</head>
<body>
<h1>模板内容</h1>
</body>
</html>

若每个app下存放静态文件的目录名均为static,静态文件的加载优先级模板的查找优先级是一样的原理,{% static "a.css" %}查找优先级为

1、djanog首先会先依次检索STATICFILES_DIRS列表中的目录
2、如果每个app下存在名为static的目录,则

为了避免出现冲突,与模板的组织原理一样,我们可以在每个static下创建子目录来充当名称空间的作用,所以上述目录结构调整为

/Users/linhaifeng/PycharmProjects/EgonPro
├── app01
│ ├── static
│ │ └── app01
│ │ └── a.css
├── app02
│ ├── static
│ │ └── app02
│ │ └── a.css
├── static
│ └── base
│ └── a.css
├── static1
│ └── base1
│ └── a.css
├── templates
│ └── index.html

模板index.html中引入静态文件a.css的路径如下,即便静态文件重名,也不会发生冲突

<link rel="stylesheet" href="{% static "base/a.css" %}">

<link rel="stylesheet" href="{% static "base1/a.css" %}">

<link rel="stylesheet" href="{% static "app01/a.css" %}">

<link rel="stylesheet" href="{% static "app02/a.css" %}">
上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术