Django模板语言(译)
原文地址:https://docs.djangoproject.com/zh-hans/2.1/ref/templates/language/
翻译日期:2019年3月8日-2019年3月9日
by:桂浮云(laurelclouds@163.com)
转载请保留以上,谢谢
本文档描述了Django模板系统的语法。如果您找的是模板语言的工作机制和功能扩展等方面的更多技术观点,请参阅:Django模板语言:程序员适用 部分。
Django模板语言的设计目标是平衡功能性和易用性,让习惯HTML的人更容易接受。如果您接触过其他文本类模板语言,例如Smarty或Jinja2,那么应该更容易认同Django模板。
理念 如果您有编程背景,或者习惯于将代码直接混编到HTML之中,那么首先要记住:Django模板系统不是简单的将Python代码内嵌于HTML之中。设计模板系统是为了实现简化呈现,而不是简化编程逻辑。 Django模板系统提供了结构标记(功能与其他编程结构类似)——if标记用于布尔测试;for标记用于循环,等等。但这些结构并非是简单地作为相应Python代码执行,而且模板系统也不能随意执行Python的表达式。模板系统默认仅支持以下标记、过滤器和语法(如有必要,可以添加自定义扩展到模板语言之中)。
模板
一个模板简单来说就是一个文本文件,可用来产出任意基于文本格式的内容(如HTML、XML、CSV等)。
模板中通常包含变量(模板求值时将会替换为对应的值)和标记(用于控制模板逻辑)。
下面基于一些基本元素给出了一个最小化的模板范例,其中涉及的元素将在后面进行解释。
{% extends "base_generic.html" %} {% block title %}{{ section.title }}{% endblock %} {% block content %} <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 %}
理念 为什么要用基于文本的模板,而不是基于XML的(如Zope的TAL)?这是因为我们想让Django模板语言不仅可用于XML/HTML,也可以用于邮件、JavaScript和CSV等,当然也包括任意基于文本格式的模板。 哦,另外就是:让人来编辑XML这件事简直与施虐无异。
变量
模板变量看起来类似于:{{ variable }}。当模板引擎发现模板变量的时候,就会计算变量variable的值并将模板变量替换为求值结果。变量名由任意英文字母、数字和下划线组合而成,但首字母不可以是下划线。英文的句号点(.)也会出现在变量部分,尽管据点是有特殊含义的(后面有具体说明)(译者注:但仍然可以将之与前后的部分作为整体视为一个模板变量)。重要的是:变量名之中不能含有空格或标点符号。
英文句点(.)是用来访问变量属性的。
考虑以下场景
技术上,当模板系统碰到英文句点(.)时,将按以下查表顺序进行求值:
- 字典查表
- 属性或方法查表
- 数字索引查表
如果求值结果是可调用的(函数、方法等),模板系统将进行无参调用,调用结果将作为模板的值。
以上查表次序意味着,重写了字典类的对象实例可能会导致不可测的情况发生。例如,想象以下尝试基于collections.defaultdict循环的代码段:
{% for k, v in defaultdict.items %}
Do something with k and v here...
{% endfor %}
由于模板系统首先通过字典查表,因此将通过字典查表操作提供一个默认值,而不是预期的(defaultdict对象实例的).items方法。这种情况下,可以视同把defaultdict对象首先转换为了dictionary对象。(本段可能存在理解错误,敬请批评指正)
以上示例中,{{ section.title }} 将被section对象的title属性所替换。
如果使用了不存在的变量,模板系统将插入string_if_invalid选项的值——也即默认为''(空字符串)。
注意:模板上下文里有bar存在,类似{{ foo.bar }}的模板表达式中的bar,将被解释为一个字面的字符串,而不是bar变量的值
以下划线开头的变量属性将不能访问,因为通常这种属性被视为私有属性。
过滤器
过滤器用来改变变量的显示方式。
过滤器是{{ name|lower }}这样的,模板变量{{ name }}的值如何显示,是通过其后的lower过滤器确定的,lower过滤器实现文本转换为小写字母的转换。其中的竖线管道符号(|)表示启用一个过滤器。
“链式”过滤是可以的,也即一个过滤器的输出可以作为下一个过滤器的输入。{{ text|escape|linebreaks }} 是一种常见的习惯用法,用于转移文本内容,然后将换行符转换为<p>标记。
有些过滤器是带参数的。例如:{{ bio|truncatewords:30 }},表示显示bio变量的前30个单词。
过滤器参数如果包含空格的话,必须用引号括起来。如,将一个列表的内容用逗号(,)和空格连接在一起,可以这样做:{{ list|join:", " }}。
django内置了大约60个过滤器。所有过滤器可参见内置过滤器参考。等不及要尝尝鲜的话,这里有一些更为常用的模板过滤器:
default
如果一个变量为false或空,则使用给定的默认值。否则,使用变量值。如:
{{ value|default:"nothing" }}
如果value变量没有提供或者为空,上面的代码将显示“nothing”。
length
返回值的长度。用于字符串和列表list。如:
{{ value|length }}
如果变量value为['a', 'b', 'c', 'd'],上面代码输出为4。
filesizeformat
将值显示为便于人类阅读的文件大小(如13 KB,4.1 MB,102 bytes,等等)。如:
{{ value|filesizeformat }}
如果变量value等于123456789,输出为:117.7 MB。
再次说明,以上只是一小部分示例,完整过滤器列表参见内置过滤器参考。
参见
django管理接口可以包含指定站点所有可用的模板标记和过滤器的一个完整引用,参见django管理文档生成器。
标记
标记如{% tag %}所示。相对而言,标记要比变量更为复杂:有些标记用于创建输出文本;有些用来控制循环或逻辑流;有些则将外部信息加载到模板中以供以后的变量使用。
有些标记需要开始标记和结束标记配对使用(即:{% tag %} ……标记内容……{% endtag %})。
Django附带了大约24个内置模板标记。您可以在内置标记参考中阅读所有相关内容。尝鲜需要,以下是一些较为常用的标记:
迭代循环访问数组中的每一项。例如,列表显示athlete_list中的athetes:
<ul> {% for athlete in athlete_list %} <li>{{ athlete.name }}</li> {% endfor %} </ul>
if,elif及else
检验变量的值,如果为true,则显示代码块的内容:
{% if athlete_list %}
Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
Athletes should be out of the locker room soon!
{% else %}
No athletes.
{% endif %}
以上,如果althlete_list非空,通过{{ athlete_list|length }}就可以显示出althlete_list中包含的athlete的个数。否则,如果athlete_in_locker_room_list非空,将显示“Athletes should be ...”的消息内容。如果athlete_list和athlete_in_locker_room_list两个变量都为空,显示内容为“No athletes”。
过滤器和各类操作符也可以用于if标记之中:
{% if athlete_list|length > 1 %}
Team: {% for athlete in athlete_list %} ... {% endfor %}
{% else %}
Athlete: {{ athlete_list.0.name }}
{% endif %}
尽管以上代码有效,但要注意大多数模板过滤器返回值为字符串,因此,使用过滤器时进行数学比较通常无法按预期那样正常运行。length过滤器是个例外。
设置模板继承(见下),是一种减少模板“样板”的有力方式。
再次声明,以上只是内置标记中的一小部分标记;完整列表参见内置标记参考。
同时,我们还可以创建自定义模板标记,参见自定义模板的标签和过滤器
参见
django管理接口可以包含指定站点所有可用的模板标记和过滤器的一个完整引用,参见django管理文档生成器。
注释
模板中注释行内部分内容,其语法为:{# #}。
例如,下面的模板绘制内容为“hello”。
{# greeting #}hello
注释内可以包含任意的模板代码,无论有效与否。例如:
{# {% if foo %}bar{% else %} #}
该语法仅用于单行注释({#和#}之间不允许换行)。如果需要注释多行模板内容,可参见comment标记。
模板继承
最最强有力以及最最复杂的django模板引擎部分就是模板继承。模板继承允许您构建一个基础“骨架”模板,其中包含站点的所有常用元素,并通过定义block,以便于子模板覆盖这些block的内容。
要理解模板继承,最简单的办法是拿范例说话:
<!DOCTYPE html> <html lang="en"> <head> <link rel="stylesheet" href="style.css"> <title>{% block title %}My amazing site{% endblock %}</title> </head> <body> <div id="sidebar"> {% block sidebar %} <ul> <li><a href="/">Home</a></li> <li><a href="/blog/">Blog</a></li> </ul> {% endblock %} </div> <div id="content"> {% block content %}{% endblock %} </div> </body> </html>
将上面的代码保存为base.html,该模板定义了一个简单的HTML骨架文档,可能包含着简单的两列形式的布局。对于子模板而言,就是将有关内容填充到一个个空白的block块之中。
该例中,block标记预先定义了准备让子模板填充的三块内容。所有block标记的作用就是告诉模板引擎,模板这些部分将会由子模板重写。
子模板大概就这个样子:
{% extends "base.html" %} {% block title %}My amazing blog{% endblock %} {% block content %} {% for entry in blog_entries %} <h2>{{ entry.title }}</h2> <p>{{ entry.body }}</p> {% endfor %} {% endblock %}
此处的extends标记很关键,是用来通知模板引擎该模板“扩展”了另外一个模板。当模板系统就该模板求值之时,首先会找出它的父模板——该例中为base.html。
这个时候,模板引擎会注意到base.html中有三个block标记,于是将块的内容替换为子模板定义的内容。根据blog_entries变量的值,输出结果可能会是这样的:
<!DOCTYPE html> <html lang="en"> <head> <link rel="stylesheet" href="style.css"> <title>My amazing blog</title> </head> <body> <div id="sidebar"> <ul> <li><a href="/">Home</a></li> <li><a href="/blog/">Blog</a></li> </ul> </div> <div id="content"> <h2>Entry one</h2> <p>This is my first entry.</p> <h2>Entry two</h2> <p>This is my second entry.</p> </div> </body> </html>
注意:由于子模板未定义sidebar块,因此生成的结果使用父模板定义的siderbar块来求值。父模板{% block %}标记中的内容将始终作为后备内容使用(以防不时之需)。
可以如需定制多层继承。通用的方法为以下三层的方案:
- 创建一个base.html模板,以满足站点主要的观感需要;
- 为每个“片段”,创建一个base_片段名.html模板;比如:base_news.html,base_sports.html。这些模板全部继承于(扩展自)base.html,并包含片段具体的风格和设计。
- 为每种页面类型创建单独的模板,比如新闻文章或者博客条目。这些模板继承于(扩展自)有关片段模板。
该方案最大化地复用代码,并简化了添加项目到共享内容区域的工作,比如说,片段范围内的导航
关于继承,这里还有以下提示:
- 如果模板中使用{% extends %}标记,那么该标记必须为当前模板中的第一个标记。否则,模板继承不能运行。
- 基础模板(如base.html)中{% block %}标记越多越好。记住,子模板不必定义所有父模板中的块,因此在一些块中填充默认内容是明智的,如有必要再在子模板中定义这些块。多比少好,有备无患。
- 如果发现一些模板中复制了相同内容,可能意味着应该将这些内容移动到父模板的一个{% blcok %}块中。
- 如果想使用父模板定义的块内容,可采用{{ block.super }}实现。这对于想使用父模板块内容但并不是完全覆盖它的时候非常有用。以{{ block.super }}插入的数据不会自动转义(见下节),因为数据已经转义过;如确有必要,请在父模板中修改实现。
- {% block %}块外以as标记定义的变量不能再块内使用。例如:下面的模板啥也没绘制:
{% trans "Title" as title %}
{% block content %}{{ title }}{% endblock %}
- 为了便于阅读,{% endblock %}标记中可以选择是否加注块名。例子:
{% block content %}
...
{% endblock content %}
在较大的模板中,这样做有助于明白结束的是对应的哪一个{% block %}标记。
最后,提醒注意,不要在同一模板中定义多个同名的block标记。该限制适用于块标记以“双向”模式运行,也即,块标记不是仅仅(由父模板)挖了个坑备(子模板同名块)填——也定义了父模板同名块用于填坑时用到的(子模块)内容。如果一个模板中存在两个同名的块标记,父模板就会搞不清究竟该使用哪个块的内容。
HTML自动转义
模板产出HTML时,总会存在字符串变量对结果HTML的影响风险,比如说,考虑以下模板代码:
Hello, {{ name }}
起初,看起来就是用一种无害的方式显示用户名而已,但你认为如果用户使用下面的代码作为名字时将发生什么:
<script>alert('hello')</script>
以此作为name变量的值时,模板得出的结果为:
Hello, <script>alert('hello')</script>
呃……,这意味着浏览器将弹出JavaScript alert对话框!
与之类似,如果名字里包含<符号,如下:
<b>username
模板绘制结果为:
Hello, <b>username
……,对网页剩余部分的内容将会统统字体加粗!
显然,用户提交的数据不应盲目相信并直接插入到您的网页中,因为恶意用户可能会利用这种漏洞来做坏事。此类安全漏洞称为 跨站点脚本(XSS)攻击。
要避免此类问题,有两个选项:
- 一是确保每个不确信变量都通过escape过滤器(见下)进行转义,从而让某些有害HTML字符无害化。对django来说,最初的几年里默认采用就是这种解决方案。但问题是这种解决方案把问题推给了开发者/模板编写者,由他们负责转义。但转义数据本来就是一件容易忘记去做的事儿。
- 二是采用django的自动HTML转义。本节以下部分将讨论自动转义的运行机制。
django默认将每个模板中的每个变量标记的输出自动转义。具体而言,将自动转义以下5个字符:
- < 转换为<
- > 转换为>
- '(单引号) 转换为'
- "(双引号) 转换为"
- & 转换为 &
再次说明,我们强调这种转义操作时默认打开。只要用的是django模板系统,就是被保护的。
如何关闭自动转义
如果您不想让数据自动转义,在每个站点、模板或变量层级上,有几个不同的关闭方法。
为啥要关闭?因为有些时候我们的目的就是将模板变量直接生成原始的HTML,这种情况下,当然不希望内容被转义。例如:可能在数据库中保存了一大块HTML数据,并希望将它直接内置在模板之中;又或者,你用django模板系统输出的文本并不是HTML文本——如邮件消息等等。
>>针对单独变量而言
关闭某个单独变量的自动转义,可以使用safe过滤器:
This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}
确信变量直接解释为HTML无害,又可以保证将来的变量仍然可以自动转义,这个时候就可以使用safe过滤器。该例中,如果data为'b',输出为:
This will be escaped: <b> This will not be escaped: <b>
>>针对模板块而言
要控制模板中的自动转义行为,请采用autoescape标记,将模板(或模板中的特定部分)打包在其内,就像这个样子:
{% autoescape off %}
Hello {{ name }}
{% endautoescape %}
autoescape标记可以附带on或off参数。有时可能会想在禁用自动转义的时候打开自动转义,这时见以下范例:
Auto-escaping is on by default. Hello {{ name }}
{% autoescape off %}
This will not be auto-escaped: {{ data }}.
Nor this: {{ other_data }}
{% autoescape on %}
Auto-escaping applies again: {{ name }}
{% endautoescape %}
{% endautoescape %}
自动转义标记既可以将效果加载到继承的子模板中,也可以影响通过include标记加载的模板,就像对块标记的影响一样。例如:
base.html
{% autoescape off %} <h1>{% block title %}{% endblock %}</h1> {% block content %} {% endblock %} {% endautoescape %}
child.html
{% extends "base.html" %} {% block title %}This & that{% endblock %} {% block content %}{{ greeting }}{% endblock %}
由于base.html模板中自动转义已经关闭,子模板中也会是关闭状态。当greeting变量值为“<b>Hello!</b>”时,将输出如下结果:
<h1>This & that</h1> <b>Hello!</b>
注意
通常模板作者不需要过度担心自动转义问题。Python(视图和自定义过滤器)端的开发者需要才需要考虑数据不应被转义的情况,并使用适当标记。因此,模板的工作由模板来解决就好。
如果你的模板不确定其是否处于自动转义已经开启的情况,那就使用escape过滤器过滤那些需要转义的变量。当自动转义开启时,不用担心escape过滤器会双重转义数据的风险——escape过滤器对自动转义过的变量不会产生效果。
字符串文字及自动转义
早前提及,过滤器参数可以为字符串:
{{ data|default:"This is a string literal." }}
所有字符串文字都会不经任何自动转义而直接插入到模板之中——就像插入的时候使用了safe过滤器那样。背后的原因是模板作者控制着字符串文字的内容,以便于编写模板时就确定文本内容是否被正确转义。
这也意味着,应该这样写:
{{ data|default:"3 < 2" }}
而不是这样:
{{ data|default:"3 < 2" }} {# Bad! Don't do this. #}
这不会对变量本身的数据造成影响。如有需要,变量内容仍将自动转义,因为对变量的转义已经超出了模板作者的控制范畴(译者注:也不应受模板作者的控制,而应该视模板变量所在的地方所处环境是否开启了自动转义)。
进行方法调用
从模板的角度来说,调用对象的大多数方法也是可行的。这意味着,相对于访问类的属性(如字段名)和变量而言,模板可以做的更多。例如,django ORM为查找外键关联对象集合提供了“entry_set”语法,为此,如果一个包含task外键的comment模型,可用以下方式循环遍历一个给定task的所有comment:
{% for comment in task.comment_set.all %}
{{ comment }}
{% endfor %}
同样的,QuerySet提供了count()方法,用于统计所包含对象的个数。因此,计算当前task相关的所有comment的个数,可以这样做:
{{ task.comment_set.all.count }}
并且,还可以简便访问模型中明确自定义的方法:
models.py
class Task(models.Model): def foo(self): return "bar"
template.html
{{ task.foo }}
由于Django故意限制了模板语言中可用的逻辑处理量,所以在模板中传递参数到调用方法时不可能的。数据应该在views中进行计算,然后再传值给模板进行显示。
自定义标记和过滤器库
某些应用提供自定义标记和过滤器库。如需访问,则首先要确定应用在INSTALLED_APPS内(例如,我们要添加了'django.contrib.humanize'),然后使用load标记进行加载:
{% load humanize %}
{{ 45000|intcomma }}
如上,load标记加载了humanize标记库,以便使用其中的intcomma过滤器。如果启用了django.contrib.admindocs,你可在安装的自定义库列表中查看管理文档区域。
load标记可带多个库名,以空格分隔。如:
{% load humanize i18n %}
参见:自定义模板(template)的标签(tags)和过滤器(filters)。编写自定义模板库时可以参考下。
自定义库和模板继承
加载自定义标记或过滤库时,标记/过滤器仅对当前模板可用——不包括模板继承路径中的任何父或子模板。
例如,如果foo.html模板包含{% load humanize %},子模板(包含{% extends "foo.html" %}不能访问humanize的模板标记和过滤器。子模板有责任自行加载自己的{% load humanize %}。
这是一个基于可维护性和符合常理目的的特性。
参见
模板参考
包括内置标记,内置过滤器,另类模板、语言的使用等等
浙公网安备 33010602011771号