Django模板
在前一章中,您可能已经注意到我们在示例视图中返回文本的一些特殊情况。 也就是说,HTML是直接在我们的Python代码中硬编码的,如下所示:
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
虽然这个技巧对于解释视图是如何工作的很方便,但是直接在你的视图中直接硬编码HTML并不是一个好主意。 原因如下:
-
对页面设计的任何改变都需要修改Python代码。 网站的设计往往比底层的Python代码更加频繁,所以如果设计改变而不需要修改Python代码,那将是很方便的。
-
这只是一个非常简单的例子。 一个普通的网页模板有数百行HTML和script脚本。 从这个混乱中解开和解决程序代码是一个噩梦。
-
编写Python代码和设计HTML是两个不同的学科,大多数专业的Web开发环境将不同的人(甚至不同的部门)之间的责任分开。 设计人员和HTML / CSS编码人员不应该被要求编辑Python代码来完成他们的工作。
-
如果程序员可以使用Python代码,设计人员可以同时使用模板,而不是等待另一个人完成编辑包含Python和HTML的单个文件,那么效率最高。
由于这些原因,将页面的设计与Python代码本身分开,使它更简洁,更易于维护。 我们可以用Django的模板系统来做到这一点,我们将在本章中讨论这个模板系统。
模板系统基础
Django模板是一个文本字符串,旨在将文档与其数据分开。 一个模板定义了占位符和各种基本逻辑(模板标签)的各个部分,用来管理文档应该如何显示。 通常,模板用于生成HTML,但Django模板同样能够生成任何基于文本的格式。
Django模板背后的哲学
如果您有编程背景,或者习惯了将编程代码直接混合到HTML中的语言,那么您应该记住,Django模板系统不仅仅是嵌入到HTML中的Python。 这种设计是:模板系统是为了表示图像,而不是程序逻辑。
我们从一个简单的示例模板开始。 这个Django模板描述了一个HTML页面,感谢一个人向公司下订单。 把它想成一个表格信:
<html>
<head>
<title>Ordering notice</title>
</head>
<body>
<h1>Ordering notice</h1>
<p>Dear {{ person_name }},</p>
<p>Thanks for placing an order from {{ company }}. It's scheduled to ship on {{ s\
hip_date|date:"F j, Y" }}.</p>
<p>Here are the items you've ordered:</p>
<ul>
{% for item in item_list %}
<li>{{ item }}</li>{% endfor %}
</ul>
{% if ordered_warranty %}
<p>Your warranty information will be included in the packaging.</p>
{% else %}
<p>
You didn't order a warranty, so you're on your own when the products inevitably stop working.
</p>
{% endif %}
<p>Sincerely,<br />{{ company }}</p>
</body>
</html>
这个模板是基于HTML代码编写,其中包含了一些变量和模板标签。让我们一起来看看它:
- 被一对大括号包围的任何文本(例如{{person_name}})是一个变量。这意味着“插入具有给定名称的变量的值”。我们如何指定变量的值?我们马上就会明白这一点。
- 任何被花括号和百分号包围的文本(例如,{% if ordered_warranty%})是一个模板标签。标签的定义非常广泛:标签只是告诉模板系统去“做些什么”。
- 这个示例模板包含一个for标签({% for item in item_list %})和一个if标签({%if ordered_warranty%})。 for标签的工作原理与Python中的for语句非常相似,可以循环使用序列中的每个项目。如你所期望的那样,一个if标签作为一个逻辑的“if”语句。在这种情况下,标签检查ordered_warranty变量的值是否为True。如果是,则模板系统将显示{%if ordered_warranty%}和{%else%}之间的所有内容。如果不是,模板系统将显示{%else%}和{%endif%}之间的所有内容。请注意,{%else%}是可选的。
- 最后,这个模板的第二段包含一个过滤器的例子,这是改变变量格式的最方便的方法。在这个例子中,{{ ship_date|date:"F j, Y" }},我们将ship_date变量传递给日期过滤器,给日期过滤器赋予参数“F j,Y”。日期过滤器格式的日期格式是指定的格式。过滤器使用管道字符(|)作为对Unix管道的引用。
每个Django模板都可以访问几个内置的标签和过滤器,其中很多将在下面的章节中讨论。 附录E包含标签和过滤器的完整列表,最好熟悉这个列表,这样你就知道什么是可能的。 也可以创建自己的过滤器和标签; 我们将在第8章中介绍。
使用模板系统
Django项目可以配置一个或多个模板引擎(如果不使用模板,甚至可以设置为零)。 Django为其自己的模板系统(Django模板语言(DTL))提供了一个内置的后端。 Django 1.11还包括对流行的Jinja2的选择性支持。
如果您没有迫切的理由选择另一个后端,那么您应该使用DTL--尤其是在您正在编写可插入应用程序并且您打算分发模板时。 Django的contrib应用程序包含模板(如django.contrib.admin),使用DTL。 本章中的所有例子都将使用DTL。 有关更高级的模板主题(包括配置第三方模板引擎),请参阅第8章。
在我们开始在你的视图中实现Django模板之前,先让我们在DTL里面挖掘一下,看看它是如何工作的。 以下是您可以在Python代码中使用Django模板系统的最基本的方法:
- 通过将原始模板代码作为字符串提供来创建模板对象。
- 使用给定的一组变量(上下文)调用Template对象的render()方法。 这将以字符串形式返回完全呈现的模板,并根据上下文评估所有变量和模板标记。 使用Django自定义Python shell(python manage.py shell),看起来像这样:
>>> from django import template
>>> t = template.Template('My name is {{ name }}.')
>>> c = template.Context({'name': 'Nige'})
>>> print (t.render(c))
My name is Nige.
>>> c = template.Context({'name': 'Barry'})
>>> print (t.render(c))
My name is Barry.
以下各节将更详细地介绍每个步骤。
一个特殊的Python提示符
如果你之前使用过Python,你可能会想知道为什么我们要运行python manage.py而不是python(或python3)。两个命令都会启动交互式解释器,但是manage.py shell命令有一个关键区别:在启动解释器之前,它会告诉Django要使用哪个设置文件。
Django的许多部分,包括模板系统,都依赖于你的设置,除非框架知道使用哪个设置,否则你将无法使用它们。如果你好奇的话,下面是它在后台的工作原理。 Django查找名为DJANGO_SETTINGS_MODULE的环境变量,应该将其设置为settings.py的导入路径。例如,假设mysite在Python路径上,DJANGO_SETTINGS_MODULE可能被设置为“mysite.settings”。
当你运行python manage.py shell时,该命令会为你设置DJANGO_SETTINGS_MODULE。在这些例子中你将需要使用python manage.py shell,否则Django会抛出异常。
创建模板对象
创建一个Template对象最简单的方法是直接实例化它。 Template类位于django.template模块中,构造函数接受一个参数,即原始模板代码。 让我们深入Python交互式解释器,看看它是如何在代码中工作的。 从第1章中创建的mysite项目目录中,输入python manage.py shell启动交互式解释器。
我们来看看一些模板系统的基础知识:
>>> from django.template import Template
>>> t = Template('My name is {{ name }}.')
>>> print (t)
如果你正在交互式命名窗口中,你会看到这样的东西:
<django.template.base.Template object at 0x030396B0>
0x030396B0每次都会有所不同,而且不相关; 这是一个Python的东西(如果你必须知道,这是Python为Template对象定义的一个“身份”,即内存地址)。
创建模板对象时,模板系统将原始模板代码编译为内部优化表单,以供呈现。 但是,如果您的模板代码包含任何语法错误,则对Template()的调用将导致TemplateSyntaxError异常:
>>> from django.template import Template
>>> t = Template('{% notatag %}')
Traceback (most recent call last):
File "", line 1, in ?
...
django.template.exceptions.TemplateSyntaxError: Invalid block tag on line 1: 'notatag'. Did you forget to register or load this tag?
这里的术语“块标记”是指{%notatag%}。 “块标签”和“模板标签”是同义词。 系统为以下任何情况引发TemplateSyntaxError异常:
- Invalid tags
- Invalid arguments to valid tags
- Invalid filters
- Invalid arguments to valid filters
- Invalid template syntax
- Unclosed tags (for tags that require closing tags)
呈现模板
一旦你有了一个Template对象,你可以通过给它一个上下文来传递它的数据。 上下文只是一组模板变量名称及其相关的值。 一个模板使用它来填充它的变量并评估它的标签。 上下文在Django中由Context类生成,它位于django.template模块中。 它的构造函数有一个可选的参数:将变量名映射到变量值的字典。 使用上下文调用Template对象的render()方法来“填充”模板:
>>> from django.template import Context, Template
>>> t = Template('My name is {{ name }}.')
>>> c = Context({'name': 'Stephane'})
>>> t.render(c)
'My name is Stephane.'
字典和上下文
Python字典是已知键和变量值之间的映射。 上下文类似于字典,但是上下文提供了额外的功能,如第8章所述。
变量名称必须以字母(A-Z或a-z)开头,可能包含更多字母,数字,下划线和点。 (点是我们稍后会遇到的特例。)变量名称区分大小写。 下面是一个模板编译和渲染的例子,使用与本章开头部分相似的模板:
>>> from django.template import Template, Context
>>> raw_template = """<p>Dear {{ person_name }},</p>
...
... <p> Thanks for placing an order from {{ company }}. It's scheduled to
... ship on {{ ship_date|date:"F j, Y" }}.</p>
...
... {% if ordered_warranty %}
... <p>Your warranty information will be included in the packaging.</p>
... {% else %}
... <p> You didn't order a warranty, so you're on your own when
... the products inevitably stop working.</p>
... {% endif %}
...
... <p>Sincerely,<br />{{ company }}</p>"""
>>> t = Template(raw_template)
>>> import datetime
>>> c = Context({'person_name': 'John Smith',
... 'company': 'Outdoor Equipment',
... 'ship_date': datetime.date(2017, 7, 2),
... 'ordered_warranty': False})
>>> t.render(c)
"<p>Dear John Smith,</p>\n\n<p>Thanks for placing an order from Outdoor Equipment. It\
's scheduled to\nship on July 2,2017.</p>\n\n\n<p>You didn't order a warranty, so you\
're on your own when\nthe products inevitably stop working.</p>\n\n\n<p>Sincerely,<br\
/>Outdoor Equipment</p>"
- 首先,我们导入模板和上下文,它们都位于模块django.template中。
- 我们将模板的原始文本保存到变量raw_template中。请注意,我们使用三重引号来指定字符串,因为它包装了多行;相反,单引号内的字符串不能包含多行。
- 接下来,我们通过将raw_template传递给Template类的构造函数来创建一个模板对象t。
- 我们从Python的标准库中导入日期时间模块,因为我们将在下面的语句中使用它。
- 然后,我们创建一个Context对象,c。 Context构造函数需要一个Python字典,它将变量名称映射到值。在这里,例如,我们指定person_name是“John Smith”,公司是“Outdoor Equipment”,等等。
- 最后,我们在模板对象上调用render()方法,将其传递给上下文。这将返回呈现的模板,即它将模板变量替换为变量的实际值,并执行任何模板标记。请注意,由于ordered_warranty变量评估为False,因此显示“您没有订购保修”段落。另请注意,日期为2017年7月2日,根据格式字符串“F j,Y”显示。 (稍后我会解释日期过滤器的格式字符串。)
如果您是Python新手,您可能会想知道为什么此输出包含换行符(“\ n”)而不是显示换行符。 这是因为Python交互式解释器中的一个微妙之处:对t.render(c)的调用返回一个字符串,默认情况下交互式解释器显示字符串的表示形式,而不是字符串的打印值。 如果要查看带换行符的字符串而不是“\ n”字符,请使用print函数:print(t.render(c))。
这些是使用Django模板系统的基础:只需编写一个模板字符串,创建一个Template对象,创建一个Context,然后调用render()方法。
多个上下文,相同的模板
一旦你有一个模板对象,你可以通过它来渲染多个上下文。 例如:
>>> from django.template import Template, Context
>>> t = Template('Hello, {{ name }}')
>>> print (t.render(Context({'name': 'John'})))
Hello, John
>>> print (t.render(Context({'name': 'Julie'})))
Hello, Julie
>>> print (t.render(Context({'name': 'Pat'})))
Hello, Pat
无论何时使用相同的模板源渲染多个上下文,只需创建一次Template对象,然后多次调用render()就可以了:
# Bad
for name in ('John', 'Julie', 'Pat'):
t = Template('Hello, {{ name }}')
print (t.render(Context({'name': name})))
# Good
t = Template('Hello, {{ name }}')
for name in ('John', 'Julie', 'Pat'):
print (t.render(Context({'name': name})))
Django的模板解析速度相当快。 在幕后,大部分解析都是通过调用一个正则表达式来实现的。 这与基于XML的模板引擎形成了鲜明的对比,这引起了XML解析器的开销,并且往往比Django的模板渲染引擎要慢几个数量级。
上下文变量查找
在目前为止的例子中,我们已经在上下文中传递了简单的值 - 主要是字符串,还有一个datetime.date的例子。 但是,模板系统优雅地处理更复杂的数据结构,如列表,词典和自定义对象。 遍历Django模板中复杂数据结构的关键是点字符(“.”)。
使用点来访问对象的字典键,属性,方法或索引。 举几个例子来说明这一点。 例如,假设你将一个Python字典传递给一个模板。 要通过字典键访问该字典的值,请使用点:
>>> from django.template import Template, Context
>>> person = {'name': 'Sally', 'age': '43'}
>>> t = Template('{{ person.name }} is {{ person.age }} years old.')
>>> c = Context({'person': person})
>>> t.render(c)
'Sally is 43 years old.'
同样,点也允许访问对象属性。 例如,一个Python datetime.date对象具有year,month和day属性,您可以使用一个点来访问Django模板中的这些属性:
>>> from django.template import Template, Context
>>> import datetime
>>> d = datetime.date(2017, 5, 2)
>>> d.year
2017
>>> d.month
5
>>> d.day
2
>>> t = Template('The month is {{ date.month }} and the year is {{ date.year }}.')
>>> c = Context({'date': d})
>>> t.render(c)
'The month is 5 and the year is 2017.'
这个例子使用了一个自定义类,演示了变量点也允许在任意对象上访问属性:
>>> from django.template import Template, Context
>>> class Person(object):
... def __init__(self, first_name, last_name):
... self.first_name, self.last_name = first_name, last_name
>>> t = Template('Hello, {{ person.first_name }} {{ person.last_name }}.')
>>> c = Context({'person': Person('John', 'Smith')})
>>> t.render(c)
'Hello, John Smith.'
点也可以指对象的方法。 例如,每个Python字符串都有方法upper()和isdigit(),您可以使用相同的点语法在Django模板中调用这些方法:
>>> from django.template import Template, Context
>>> t = Template('{{ var }} -- {{ var.upper }} -- {{ var.isdigit }}')
>>> t.render(Context({'var': 'hello'}))
'hello -- HELLO -- False'
>>> t.render(Context({'var': '123'}))
'123 -- 123 -- True'
请注意,在方法调用中不包括括号。 另外,不可能将参数传递给方法; 你只能调用没有必要参数的方法。 (我在本章后面解释这个理念。)最后,点也被用来访问列表索引,例如:
>>> from django.template import Template, Context
>>> t = Template('Item 2 is {{ items.2 }}.')
>>> c = Context({'items': ['apples', 'bananas', 'carrots']})
>>> t.render(c)
'Item 2 is carrots.'
负值列表索引是不允许的。 例如,模板变量{{items.-1}}将导致TemplateSyntaxError。
Python列表索引
提醒:Python列表有从0开始的索引。 第一项是索引0,第二项是索引1,依此类推。
点查找可以这样概括:当模板系统在变量名中遇到一个点时,它会按以下顺序尝试以下查找:
- 字典查找(例如,foo [“bar”])
- 属性查找(例如,foo.bar)
- 方法调用(例如,foo.bar())
- 列表索引查找(例如,foo [2])
系统使用可用的第一个查找类型。 这是短路逻辑。 点查找可以嵌套多层。 例如,下面的例子使用{{person.name.upper}},它翻译成字典查找(person ['name']),然后是方法调用(upper()):
>>> from django.template import Template, Context
>>> person = {'name': 'Sally', 'age': '43'}
>>> t = Template('{{ person.name.upper }} is {{ person.age }} years old.')
>>> c = Context({'person': person})
>>> t.render(c)
'SALLY is 43 years old.'
方法调用行为
方法调用比其他查找类型稍微复杂一些。 这里有一些事情要记住:
- 如果在方法查找期间,方法引发异常,则将传播异常,除非异常具有值为True的属性silent_variable_failure。 如果异常具有silent_variable_failure属性,则该变量将呈现为引擎的string_if_invalid配置选项的值(默认情况下,为空字符串)。 例如:
>>> t = Template("My name is {{ person.first_name }}.")
>>> class PersonClass3:
... def first_name(self):
... raise AssertionError("foo")
>>> p = PersonClass3()
>>> t.render(Context({"person": p}))
Traceback (most recent call last):
...
AssertionError: foo
>>> class SilentAssertionError(Exception):
... silent_variable_failure = True
>>> class PersonClass4:
... def first_name(self):
... raise SilentAssertionError
>>> p = PersonClass4()
>>> t.render(Context({"person": p}))
'My name is .'
- 只有当方法没有必要的参数时,方法调用才会起作用。 否则,系统将移动到下一个查找类型(列表索引查找)。
- 按照设计,Django有意限制了模板中可用的逻辑处理的数量,所以无法将参数传递给从模板中访问的方法调用。 数据应该在视图中计算,然后传递给模板进行显示。
- 显然,有些方法有副作用,最多也是愚蠢的,甚至可能是安全漏洞,允许模板系统访问它们。
- 比方说,你有一个拥有delete()方法的BankAccount对象。 如果模板包含{{account.delete}}之类的内容,其中account是BankAccount对象,则在模板呈现时,对象将被删除! 为了防止这种情况,在方法上设置函数属性alters_data:
def delete(self):
# Delete the account
delete.alters_data = True
模板系统不会执行任何以这种方式标记的方法。 继续上面的例子,如果一个模板包含{{account.delete}}并且delete()方法的alters_data = True,那么当模板被渲染时,delete()方法将不会被执行,引擎将代替 变量与string_if_invalid。
注意:Django模型对象上的动态生成的delete()和save()方法会自动设置alters_data = true。
如何处理无效的变量
通常,如果一个变量不存在,模板系统会插入引擎的string_if_invalid配置选项的值,默认情况下这是一个空字符串。 例如:
>>> from django.template import Template, Context
>>> t = Template('Your name is {{ name }}.')
>>> t.render(Context())
'Your name is .'
>>> t.render(Context({'var': 'hello'}))
'Your name is .'
>>> t.render(Context({'NAME': 'hello'}))
'Your name is .'
>>> t.render(Context({'Name': 'hello'}))
'Your name is .'
这种行为比引发异常更好,因为它的目的是对人为错误有弹性。 在这种情况下,所有的查找失败,因为变量名称有错误的大小写或名称。 在现实世界中,由于小的模板语法错误,网站变得不可访问是不可接受的。