Django--模板

251 阅读18分钟

「这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战」。

一.模板系统基本知识

模板是一个文本,用于分离文档的表现形式和内容。模板定义了占位符以及各种用于规范文档该如何显示的各部分基本逻辑(模板标签)。模板通常用于HTML,但是Django的模板也能产生任何基于文本格式的文档。下面是一个简单的例子模板,可将其视为一个格式信函:

<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 {{ ship_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}}称为变量(variable),这意味着在此处插入指定变量的值;
  • 被大括号和百分号包围的文本,如:{% if ordered_warranty %}是模板标签(template tag)。标签tag定义比较明确,即:仅通知模板系统完成某些工作的标签,这个例子包含了一个for标签({% for item in item_list %})和一个if标签({% if ordered_warranty %})。for标签类似Python的for语句,可以让你循环访问序列里面的每一个项目。if标签,执行逻辑判断的;
  • 这个模板的第二段有个关于filter过滤器的例子,它是一种最便捷的转换变量输出格式的方式。如这个例子中的{{ship_date|date:"F j, Y"}},我们将变量ship_date传递给date过滤器,同时指定参数F j, Y。date过滤器根据参数进行格式化输出,过滤器是用管道符|来调用的。

二.如何使用模板系统

在Python代码中使用Django模板的最基本方式如下:

  1. 可以用原始的模板代码字符串创建一个Template对象,Django同样支持用指定模板文件路径的方式来创建Template对象;
  2. 调用模板对象的render方法,并且传入一套变量context。它将返回一个基于模板的展现字符串,模板中的变量和标签会被context值替换。

代码如下:

>>> from django import template
>>> t = template.Template('My name is {{ name }}.')
>>> c = template.Context({'name': 'Adrian'})
>>> print t.render(c)
My name is Adrian.
>>> c = template.Context({'name': 'Fred'})
>>> print t.render(c)
My name is Fred.

三.创建模板对象

转入工程目录下,输入命令:python manage.py shell启动交互界面,之所以使用shell而不是直接启用python界面,是因为shell在启动解释器之前,会告诉Django使用哪个设置文件,Django框架的大部分子系统,包括模板系统,都依赖于配置文件,如果Django不知道使用哪个配置文件,这些系统将不能工作。

现在开始了解一些模板系统的基本知识:

>>> from django.template import Template
>>> t = Template('My name is {{ name }}.')
>>> print t

接着shell界面就会显示如下内容:

<django.template.Template object at 0xb7d5f24c>

0xb7d5f24c 每次都会不一样,这时Python运行时Template对象的id,当你创建一个Template对象,模板系统在内部编译这个模板到内部格式,并做优化,做好渲染的准备。如果模板语法有错误,那么在调用Template()时就会抛出TemplateSyntaxError异常:

>>> from django.template import Template
>>> t = Template('{% notatag %}')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  ...
django.template.TemplateSyntaxError: Invalid block tag: 'notatag'

这里,块标签(block tag)指向的是"{% notatag %}",块标签与模板标签是同义的。

系统会在下面的情形抛出TemplateSyntaxError异常:

  • 无效的tags;
  • 标签的参数无效;
  • 无效的过滤器;
  • 过滤器的参数无效;
  • 无效的模板语法;
  • 未封闭的块标签(针对需要封闭的块标签)。

四.模板渲染

一旦你创建一个Template对象,你可以用context来传递数据给它。一个context是一系列变量和它们值的集合。context在Django中表现为Context类,在djago.template模块里。它的构造函数带有一个可选的参数:一个字典映射变量和它们的值。调用Template对象的render()方法并传递context来填充模板:

>>> from django.template import Context, Template
>>> t = Template('My name is {{ name }}.')
>>> c = Context({'name': 'Stephane'})
>>> t.render(c)
u'My name is Stephane.'

在这里,t.Render(c)返回的值是一个Unicode对象,不是普通的Python字符串。在框架中,Django会一直使用Unicode对象而不是普通的字符串。这里Context对象跟python的字典很像,context还会提供更多的功能。变量名必须由英文字符开始(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(2009, 4, 2),
...     'ordered_warranty': False})
>>> t.render(c)
u"<p>Dear John Smith,</p>\n\n<p>Thanks for placing an order from Outdoor
Equipment. It's scheduled to\nship on April 2, 2009.</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>"

总结:

  1. 先导入类Template和类Context;
  2. 把模板原始文本保存到变量raw_template;
  3. 创建模板对象t,把raw_template作为Template类构造函数的参数;
  4. 创建一个Context对象 c,Context构造的参数是字典类型,通过键-值的形式可以指定模板里面各变量的值;
  5. 然后在模板对象c上调用render方法,传递context参数给它,这是返回渲染后的模板的方法,它会替换模板变量为真实的值和执行块标签。

注意时间显示是:April 2, 2009,它是按'F j, y'格式显示的。在这里,换行显示的是换行符\n而不是真的换行,因为这是Python交互显示器的缘故:调用t.render()返回字符串,解释器缺省显示这些字符串的真实内容呈现,而不是打印这个变量的值。

五.同一模板,多个上下文

一旦有了模板对象,就可以通过它渲染多个context:

>>> 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

只进行一次模板创建然后多次调用render()方法渲染会更为高效:

t = Template('Hello, {{ name }}')
for name in ('John', 'Julie', 'Pat'):
    print t.render(Context({'name': name}))

Django模板解析非常快捷。大部分的解析工作都是在后台通过对简短正则表达式一次性调用来完成,这和基于XML的模板引擎形成鲜明对比,那些引擎承担了XML解析器的开销,且往往比Django模板渲染引擎要慢上几个数量级。

六.深度变量的查找

在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)
u'Sally is 43 years old.'

同样也可以通过句点来访问对象的属性,访问类的属性,引用对象的方法(注意这里调用方法时没有使用圆括号,且无法给该方法传递参数,也就是说只能调用不需要参数的方法),访问列表索引(不允许使用负数列表索引),这里就不举例说明,参考上段代码。同样的,句点查找可以多级深度嵌套,例如person是一个字典,里面有一个键name,值是字符串,字符串又带有upper方法,故person.name.upper会转换成字典类型查找(person['name'].upper())。

七.方法调用行为

在方法查找过程中,如果某方法抛出一个异常,除非该异常有一个silent_variable_failure属性并且值为True,否则的话它将被传播。如果异常被传播,模板里的指定变量会被置为空字符串。如:

>>> 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(AssertionError):
...     silent_variable_failure = True
>>> class PersonClass4:
...     def first_name(self):
...         raise SilentAssertionError
>>> p = PersonClass4()
>>> t.render(Context({"person": p}))
u'My name is .

仅在方法无需传入参数时,其调用才有效。否则,系统会转移到下一个查找类型。接上面为什么方法调用只能调用无参数的方法。

如何处理无效变量:

默认情况下,如果一个变量不存在,模板系统会把它展示位空字符串,不做任何事情来表示失败,系统悄悄地表示失败,而不是引发一个异常,因为在现实中,对一个web站点来说,仅仅因为一个小的模板语法错误而造成无法访问,这是不可接受的。

八.基本的模板标签和过滤器

标签:

if/else

  • {% if %}:标签检查一个变量,如果这个变量为真,系统会显示在{% if %}和{% endif %}之间的内容;
  • {% else %}:是可选的;
  • {% if %}标签接受and,or或者not关键字来对多个变量做判断,或者对变量取反(not);
  • {% if %}标签不允许在同一个标签中同时使用and和or,因为逻辑上可能模糊的。系统不支持用圆括号来组合比较操作,如果确实需要用到圆括号来组合表达逻辑,考虑将它移到模板之外处理,然后以模板变量的形式传入结果,或者用嵌套的{% if %}标签替换;
  • 并没有{% elif %}这个标签,使用嵌套{% if %}标签实现相同效果;
  • 一定要使用{% endif %}标签来关闭每个{% if %}标签。

for

  • {% for %}允许我们在一个序列上迭代,与python的for语句的情形类似,循环语法是for x in y,每次循环,系统会渲染在{% for %}和{% endfor %}之间的所有内容;
  • 给标签增加一个reversed使得该列表被反向迭代;
  • 可以嵌套使用{% for %}标签;
  • 在执行循环前先检测列表的大小是一个通常的做法,当列表为空则输出一些特别的提示;
  • for标签支持一个可选的{% empty %}分句,通过它我们可以定义列表为空时的输出内容;
  • Django不支持退出循环操作,如果想退出循环,可以改变正在迭代的变量,让其仅仅包含需要迭代的项目,同理,Django也不支持continue语句,我们无法让当前迭代操作跳回到循环头部。

在每个{% for %}循环里面有一个称为**"forloop"**的模板变量,这个变量有一些提示循环进度信息的属性:

  • forloop.counter总是一个表示当前循环的执行次数的整数计数器,这个计数器是从1开始的,forloop.counter0类似于forloop.count,但它是从0开始计数的;
  • forloop.revcounter是表示循环中剩余项的整型变量,forloop.revcounter0跟它类似,但后者是以0作为结束索引的;
  • forloop.first是一个布尔值,如果该迭代是第一次执行,那么它被置为True,forloop.last是一个布尔值,在最后一次执行循环时被置为True;
  • forloop.parentloop是一个指向当前循环的上一级循环的forloop对象的引用(在嵌套循环的情况下);
  • forloop变量仅仅能够在循环中使用,碰到{% endfor %}标签后,forloop就不可访问了。

ifequal/ifnotequal

  • {% ifequal %}标签比较两个值,当他们相等时,显示在{% ifequal %}和{% endifequal %}之中的所有值;
  • {% ifequal %}支持可选的{% else %}标签;
  • 只有模板变量,字符串,整数和小数可以作为{% ifequal %}标签的参数,其他类型,比如字典、列表、布尔类型都是不可以作为参数的。

注释

  • 注释使用{# #},但这种语法不能跨越多行,如果要实现多行,可以使用{% comment %}模板标签。

过滤器

模板过滤器是在变量显示前修改它值的一个简单方法,过滤器使用管道字符,如下所示:

{{ name|lower }}

过滤管道可以被套接,就是说,一个过滤器管道的输出又可以作为下一个管道的输入,如此下去,下面这个例子实现查找列表的第一个元素并将其转化为大写:

{{ my_list|first|upper }}

有些过滤器有参数,过滤器的参数跟随冒号之后并总是以双引号包含,例如:

{{ bio|truncatewords:"30" }} # 显示变量bio的前30个词

以下是最为重要的过滤器的一部分:

  • addslashes:添加反斜杠到任何反斜杠、单引号或者双引号前面;
  • date:按指定的格式字符串参数格式化date或者datetime对象;
  • length:返回变量的长度。

Django模板语言的设计哲学:

  • 业务逻辑应该和表现逻辑相对分开,我们将模板系统视为控制表现及表现相关逻辑的工具,仅此而已,所以在模板里面,不可能直接调用Python代码;
  • 语法不应受到HTML/XML的束缚;
  • 假定设计师精通HTML编码;
  • 假定设计师不是Python程序员;
  • 目标不是发明一种编程语言,目标是恰到好处地提供分支和循环这一类编程式功能。

九.在视图中使用模板

9.1 模板加载

为了减少模板加载调用过程及模板本身的冗余代码,Django提供了一种使用方便且功能强大的API,用于从磁盘中加载模板。先在工程目录下创建templates文件夹,在里面创建模板文件,比如:current_time.html。然后把templates路径添加到django中,打开django中的setting.py文件,找到TEMPLATE这个数组,在DIRS中添加列表元素:[os.path.join(BASE_DIR, 'templates')],这段代码意思就是获取Django的工程目录,然后在里面寻找一个名叫'templates'的文件夹,里面就是我们所需的模板文件。添加完路径后,就可以编写模板文件及代码了:

<html>
<body>
    <h1>
        It is now {{ current_date }}.
    </h1>
</body>
</html>
# 这是模板文件
def time(request):
    now = datetime.datetime.now()
    t = get_template('current_datetime.html') # get_template函数以模板文件为参数,返回一个编译好的template对象
    html = t.render({'current_date': now})
    return HttpResponse(html)
# 这是views.py里面的代码
# 这样就能在视图中加载模板了

render_to_response()

Django提供了一个更加方便的方法,让你一次性地载入某个模板文件,渲染它,然后作为HttpResponse返回。该方法就是django.shortcuts模块中的render_to_response()函数。

def time(request):
    now = datetime.datetime.now()
    return render_to_response('current_datetime.html', {'current_date': now})

render_to_response的第一个参数必须是要使用的模板名称,如果要给定第二个参数,那么该参数必须是为该模板创建context时所使用的的字典,如果不提供第二个参数,render_to_response使用一个空字典。

locals()技巧

我们可以利用Python的内建函数locals(),它返回的字典对所有局部变量的名称与值进行映射。因此,前面的视图函数可以这样写:

def time(request):
    current_date = datetime.datetime.now()
    return render_to_response('current_datetime.html', locals())

这里面,把之前的变量名now更改为模板所预期的变量名current_date,这样locals()函数就能根据这个映射获取相应的值。使用locals()时要注意的是它将包括所有的局部变量。

get__template()中使用子目录

把模板存放于模板目录的子目录中,只需要在调用get_template()时,把子目录名和一条斜杠添加到模板名称之前,如:

t = get_template('dateapp/current_datetime.html')

对于子目录树的深度没有限制,想要多少层都可以。注意:windows用户也必须使用斜杠而非反斜杠,get_template()假定的是Unix风格的文件名符号约定。

include模板标签

利用模板加载机制的内建模板标签:{% include %},该标签允许在模板中包含其他模板的内容,标签的参数就是要包含的模板名称,可以是一个变量,也可以是单/双引号硬编码的字符串,每当在多个模板中出现相同的代码时,就要考虑使用{% include %}来减少重复。注意:所包含的模板执行时的context和包含它们的模板是一样的。

9.2 模板继承

本质上来说,模板继承就是先构造一个基础框架模板,而后在其子模板中对它所包含站点公用部分和定义块进行重载。第一步,定义基础模板,该框架之后将由子模板所继承:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <h1>My helpful timestamp site</h1>
    {% block content %}{% endblock %}
    {% block footer %}
    <hr>
    <p>Thanks for visiting my site.</p>
    {% endblock %}
</body>
</html>

将其命名为base.html,并保存到templates文件夹下。子模板的作用就是重载、添加或保留那些块的内容。我们使用一个之前见过的标签:{% block %}。所有的{% block %}标签告诉模板引擎,子模板可以重载这些部分,该模板下的这一块内容有可能被子模板覆盖。现在修改current_datetime.html模板:

{% extends "base.html" %}

{% block title %}The current time{% endblock %}

{% block content %}
<p>It is now {{ current_date }}.</p>
{% endblock %}

再为hours_ahead视图创建一个模板:

{% extends "base.html" %}

{% block title %}Future time{% endblock %}

{% block content %}
<p>In {{ hour_offset }} hour(s), it will be {{ next_time }}.</p>
{% endblock %}

这样操作之后,如果想进行站点级的设计修改,仅需要修改base.html,所有其他的模板都会做出相应改变。以下是其(模板继承)工作方式:在加载current_datetime.html模板时,模板引擎发现了{% extends %}标签,注意到该模板是一个子模板。模板引擎立即装载其父模板,即base.html。此时,模板引擎注意到base.html中的三个{% block %}标签,并用子模板的内容替换这些block。因此,引擎将会使用我们在{% block title %}中定义的标题,对{% block content %}也是如此,到此完成内容替换工作。继承并不会影响到模板的上下文,换句话说,任何处在继承树上的模板都可以访问到你传到模板中的每一个模板变量。你可以根据需要使用任意多的继承次数。使用继承的一种常见方式是下面的三层法:

  1. 创建base.html模板,在其中定义站点的主要外观感受,这些都是不常修改甚至不修改的地方;
  2. 为网站的每个区域创建base_SECTION.html模板,这些模板对base.html进行拓展,并包含区域特定的风格和设计;
  3. 为每种类型的页面创建独立的模板,这些模板拓展相应的区域模板。

以下是使用模板继承的一些诀窍:

  • 如果在模板中使用{% extends %},必须保证其为模板中的第一个模板标记,否则,模板继承将不起作用;
  • 一般来说,基础模板中的{% block %}标签越多越好,子模板不必定义父模板中的所有的代码块,因此,你可以用合理的缺省值对一些代码块进行填充,然后只对子模板所需的代码块进行重定义;
  • 如果发觉自己在多个模板之间拷贝代码,应该考虑将该代码放置到父模板的某个{% block %}中;
  • 如果你需要访问父模板中的块的内容,使用{{ block.super }}这个标签,这个魔法变量将会表现出父模板中的内容,如果只想在上级代码块基础上添加内容,而不是全部重载,该变量就显得非常有用了;
  • 不允许在同一个模板中定义多个同名的{% block %}。存在这样的限制是因为block标签的工作方式是双向的。也就是说,block标签不仅挖了一个要填的坑,也定义了父模板中这个坑所填充的内容。如果模板中出现了两个相同名称的{% block %}标签,父模板将无从得知要使用哪个块的内容;
  • {% extends %}对所传入模板名称使用的加载方法和get_template()相同。也就是说,会将模板名称添加到TEMPLATE_DIRS设置之后;
  • 多数情况下,{% extends %}的参数应该是字符串,但是如果直到运行时方能确定父模板名,这个参数也可以是个变量。

9.4 过滤器

过滤器在模板中是放在变量后并用于控制变量显示格式的技术,变量与过滤器之间通过管道符号"|"连接,Django中常用的其他过滤器如下:

  • add:给value加上一个数值;
  • addslashes:单引号加上转义号;
  • capfirst:第一个字母大写;
  • center:输出指定长度的字符串,把变量居中;
  • cut:删除指定字符串;
  • date:格式化日期;
  • default:如果值不存在,则使用默认值代替;
  • default_if_none:如果值为None,则使用默认值代替,使用方式与default类似;
  • dictsort:按某字段排序,变量必须是一个dictionary;
  • escape:按HTML转意,如将"<"转换成"&lt";
  • floatformat:转换为指定精度的小数,默认保留1位小数;
  • filesizeformat:增加数字的可读性,转换结果为1KB,9MB,3Bytes等;
  • join:用指定分隔符连接列表;
  • length_is:检查列表、字符串的长度是否符合指定的值;
  • slugify:在字符串中留下减号和下划线,其他符号删除,空格用减号替换;
  • yesno:将布尔变量变换为字符串yes、no或maybe,也可以在参数中指定变换的结果。