Django 模板语言(译)

最近在折腾Django,看到官网的Django模板文章写得挺详细的,就想着翻译下以加深自己的学习印象,如果能帮助到大家我也会感到很欣慰,以下是译文。

这篇文章主要讲解了Django模板系统语法,如果你想了解更多关于Django模板如何工作以及如何扩展的话,可以看这篇文章The Django template language: for Python programmers.

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的模板呢?我们是想让Django的模板语言用在更多的地方而不是仅仅在XML或HTML中。在联网的世界里,我们把它用在电子邮箱,JavaScript以及CSV。你可以在任何文本格式中使用模板语言。

哦,顺便说下,让人们编辑XML文件是很苦逼的。

变量

变量看起来像这样:{{ 变量名 }},当模板引擎遇到变量时,会计算这个变量,并把结果赋值给它。变量名是由任何的数字,字母以及下划线组成。点(“.”)也会出现在变量部分,当然他有特殊用途,稍后会说明。特别注意的是,变量名中不能出现空格符以及任何的标点符号。

上面说到点也会出现在变量部分,它的作用就是用来访问变量的属性的。

背景

严格来讲,当模板系统遇到点时,它会根据以下的顺序查找:

  • 字典查找

  • 属性或方法查找

  • 数字索引查找

如果结果的值是可调用,那么它就会被无参数调用。调用的结果就会变成模板中的值。

这个查找顺序会对那些覆写了字典查找的对象造成一些不可预期的行为。比如下面是遍历collections.defaultdict的代码片:

{% for k, v in defaultdict.iteritems %}
   Do something with k and v here...{% endfor %}

因为先发生字典查找,所以这不可预期的行为就发生了,它会返回defaultdict的默认值而不会调用它的iteritems()方法。在这个例子中,可以先转化成字典。

在上面的模板例子中,{{ section.title }}会被section对象的title属性代替。

如果你调用的变量不存在,那么模板系统将默认插入空字符串‘’

注意:如果模板的上下文(context)中传入“bar”变量,而模板中存在{{ foo.bar }}表达式,那么该表达式仅仅具有字面意思,即调用foo的bar属性,而不会调用context中“bar”的值。

过滤器(FIlters)

你可以用filters来改变变量最终显示的值。

过滤器看起来就像这样{{ name|lower }}。这是将name变量通过lower过滤器全部转换为小写字母。|用来调用过滤器。

过滤器可以是链式的,一个过滤器的输出会被用在下一个。{{ text|escape|linebreaks }} 是一种通用的形式用来将文本内容转义,然后再转为<P>标签以适用HTML。

有些过滤器带参数。如以下带参过滤器:{{ bio|truncatewords:30 }}意思是只展示bio变量的前30个字符。

如果过滤器参数含有空格那么参数必须被引号引起来;比如想要用逗号和空格将list中的元素拼接起来,你可以这样{{ list|join:“, ”}}。

Django提供了大约60个内置模板过滤器。你可以到built-in filter reference.查阅更多的过滤器。为了让你尝试下,下面是一些常用的过滤器:

default

如果一个变量是false或者是空的,那么它的值就是你给的默认值,否则就是变量本身的值。比如:

{{ value|default:"nothing" }}

如果变量value没有提供,或者是空的话,上面就会显示nothing

length

返回变量的长度。这对于字符串和列表都起作用。比如:

{{ value|length }}

如果 value是一个列表['a','b','c','d'],那么输出就是4。

filesizeformat

将文件大小转换为符合人们阅读习惯的格式(如:‘13 KB’,‘4.1  MB’,‘102 bytes’等)。如下:

{{ value|filesizeformat }}

如果value是123456789的话,那么输出将会是117.7MB。

重申下,这里仅是很少的例子,可以到built-in filter reference.查看更多完整列表。

当然你也可以创建自定义模板过滤器,可以查阅这篇文章Custom template tags and filters.

提示
Django 的admin交互界面使用了所有的模板标签和过滤器。你可以参考这篇文章The Django admin documentation generator.

标签(Tags)

标签的形式是 {% tag %}。标签比变量要更复杂:有些标签在输出中创建文本,有些通过展示循环和逻辑来控制流,有些引入额外的信息到模板中以供后续变量使用。

有些标签需要开头和结尾(比如:{%  tag  %}...标签内容...{% endtag %})

Django大约有两打(24个)内置模板标签。你可以查阅这篇文档built-in tag reference.。以下仅列举一些常用标签:

for

循环遍历数组或列表,比如展示名为athete_list的运动员列表:

<ul>{% for athlete in athlete_list %}    <li>{{ athete.name }}</li>{% endfor %}</ul>

if,elif,else

{% if athlete_list %}
    Number of athletes: {{ athete_list|lengh }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}

如上所示,如果athlete_list列表不为空,则会通过{{ athlete_list|length }}变量来显示列表长度。否则,如果athlete_in_locker_room_list列表不为空,则显示“Athletes should be out of the locker room soon!”信息。如果两个列表都为空,则显示“”No athletes.”。

你可以在if标签中使用过滤器以及进行一系列的操作:

{% if athlete_list|length > 1 %}
   Team: {% for athlete in athlete_list %} ... {% endfor %}{% else %}
   Athlete: {{ athlete_list.0.name }}{% endif %}

当上面的代码运行时,注意大部分模板过滤器是以字符串类型返回的,所以上面过滤器返回的length是无法与数字1进行比较的。

block,extend

用于模板继承(下面会讲到),一种 给力的方法可以在模板中减少样板的使用。

再说下,上面仅是模板标签的一部分,查看更多内容请移步至此built-intag reference

当然你可以参考这篇文档Custom template tags and filters来创建你自己的模板标签。

提示
Django 的admin交互界面使用了所有的模板标签和过滤器。你可以参考这篇文章The Django admin documentation generator.

注释

可以用{# #}来注释模板中的某一行。

如下,下面只有“hello”会在模板中起作用。

{# greeting #}hello

注释中可以包含任何模板语句,如下:

{# {% if foo %}bar{% else %} #}

上述的注释语句只能用于单行注释({ # # }该注释语句中不允许存在多行)。如果你想用多行注释,请移步至此注释

模板继承

在Django模板引擎中最给力也是最复杂的部分就是模板继承。模板继承可以使你建立一个包含网站所有公共元素的基本骨架,在里面可以定义一些区块,模板的子模板可以重写这些区块。

下面的例子可以让你更好的理解模板继承:

<!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标签都会告诉模板引擎,子模板可能会重写该区块。

下面我们定义了一个子模板:

{% 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标签,然后会将子模板的内容填充到父模板相应的区块中。最后的结果如下({% block content %}区块里具体的内容是由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区块,但还是有内容,没错这是从父模板中来的。因此父模板的{%block%}标签中的内容总是作为默认值,,即假如子模板没有重写该区块,那么就默认使用父模板中的内容。

你可以根据需要采用多层继承,一般来讲我们采用的是三层继承:

  • 首先创建一个包含网站主体外观的基础模板base.html

  • 为每部分的网页创建名为base_部分名称.html的模板,比如,base_news.html,base_sports.html。这些模板都是继承base.html模板,并且都有自己的设计样式。

  • 为每一种类型的网页创建个性化模板,比如新闻或者博客,它们都是继承上述相应的部分模板

上述的继承方案可以极大化的重用你的代码,并且方便扩展。

以下是使用模板继承需要注意的地方:

  • 如果你要在模板标签中使用{% extends %},那么它必须作为第一个模板标签,否则模板继承将失效。

  • 在你的基础模板中多使用{ % block % }。记住,在子模板中不一定都要重写父模板的block标签,所以你可以在父模板中多定义些block,在里面填充合理的默认值,这样在子模板中你可以根据需要重写相应的block,其他的使用父模板的默认值就可以了。俗话说,有备无患吗。

  • 如果你发现在很多模板中有相同的内容,那么你可以将这些相同的内容提取出来放在父模板的block中。

  • 如果你想获取父模板block中的内容,你可以使用变量{{ block.super }}。如果你想要增加父模板block的内容,而不是重写它,那么这个变量将会非常有用。使用该变量插入的数据不会被自动转义(下面会提到),因为如果有必要的话它已经在父模板中被转以了。

  • 在{ % block % }标签外面用as标签创建的变量是无法在{ % block % }内部使用的,如下,该模板不会显示任何内容:

{% trans "Title" as title %}{% block content %}{{ title }}{% endblock %}
  • 另外,你可以给 {% endblock %} 标签取任意名字,比如:

{% block content %}
...
{% endblock content %}

在很大的模板中,这可以让你知道哪个block标签正在关闭。

最后,注意你不能在同一模板中定义相同名字的block标签,这是因为block标签是双向作用的,block标签不仅仅提供区域让子模板填充,它也可以在父模板中填充相应的内容。如果在模板中有相同名字的block标签,那么,父模板将不知道到底使用哪个block中的内容来填充。

HTML自动转义

当通过模板生成HTML时,有个风险,就是变量中可能存在一些特殊字符会影响HTML的结果。比如,如下模板片段:

hello, {{ name }}

这从表面上看上去没什么问题,但是你需要考虑到,如果用户的变量name输入以下内容会发生什么:

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

如果name变量是以上内容,那么最后HTML中会是这样的结果:

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

...这意味着浏览器会弹出一个脚本对话框!

类似的,如果name变量包含‘<’字符呢,比如这样:

<b>username

这会导致网页的剩余部分加粗。

显然,用户提交的数据是不能盲目信任的,并且不能直接插入到网页中,因为会有恶意的用户利用这种漏洞来干坏事。这种安全漏洞叫做跨站脚本攻击(XSS)。

为了避免这个问题,你有两种选择:

  • 一种,你可以利用escape过滤器来运行每个不被信任的变量,这会把有潜在危险的字符转换为安全的。当然在Django推出的头几年,这是一种默认的解决方法。但是存在一个问题,你需要确保每个变量都被转义,显然我们很容易漏掉一些数据。

  • 第二种,你可以利用Django的HTML自动转义机制。这部分的剩余部分会讲解自动转义是如何工作的。

在Django中,每个模板都会默认转义每个变量,以下是五个比较特别的字符转义。

  • < 会被转义成<

  • > 会被转义成>

  • '(单引号)会被转义成'

  • " (双引号)会被转义成"

  • & 会被转义成&

在强调一遍,这种自动转义行为是默认的,当你在使用Django的模板系统时你是受保护的。

如果关闭自动转义

如果你不想要数据被自动转义,比如你不想要某个网页,或者某个模板亦或者某个变量被转义的话,你有几个方法来关闭它。

为什么你会想关闭自动转义呢?因为有时候你想把原生的HTML语句嵌入到模板变量中,在这种情况下,你是不会想要将变量里的内容转义的。举个例子,你可能在数据库中存了一点HTML,你想把它直接嵌入到模板中。或者,你可能正在用Django的模板系统生成非HTML文本,比如电子邮箱信息。

关闭变量的自动转义

可以用safe过滤器来关闭单个变量的自动转义:

这会被转义: {{ data }}这不会被转义: {{ data|safe }}

如果data变量中存在<b>,那么上面两句输出的结果是这样的:

这会被转义: <b>这不会被转义: <b>

关闭模板区块的自动转义

为了控制模板的自动转义,需要在外面套一层autoescape标签,比如:

{% autoescape off %}
    Hello {{ name }}
{% endautoescape %}

你可以用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中自动转义是关闭的,在他的子模板child.html中也将是关闭的。如果greeting变量传入字符串<b>Hello!</b>,那么模板最终的结果将会是如下:

<h1>This & that</h1><b>Hello!</b>

注意事项

通常来讲,我们不用过多的担心自动转义。只有在写views时或者自定义过滤器时需要注意自动转义,并合理的标记数据。所以尽情的使用模板吧。

如果你创建的模板在被使用时无法确定自动转义是否打开,你可以添加escape过滤器到每个需要转义的变量上。当自动转义已开启,escape过滤器对已转义的变量进行再次转义并不会对该变量产生影响。

字符串与自动转义

在前面提到,过滤器的参数可以是字符串:

{{ data|default:"This is a string literal." }}

所有在模板中的字面上的字符串都不会经过转义--他们看上去像使用了safe过滤器。这背后的原因是字符串内容是由模板创建者决定的,所以他们可以确保文本是被正确转义的。

也就是说你应该这样写:

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

而不是这样:

{{ data|default:"3 < 2" }}  {# 别这样写! #}

这不会影响变量data自身,变量的内容在必要时仍然会被自动转义,因为这不是模板作者能控制的。

访问方法调用

大多数绑定在对象上的方法调用也存在于模板中。这意味着模板可以访问的不仅仅是类的属性以及从view中传来的变量。举个例子,Django的ORM(对象关系映射)框架提供个一个叫“entry_set”语法,这可以找到由外键关联的对象集。所以如果模型“comment”有个外键关联到模型“task”的话,你可以遍历该task的所有comments:

{% for comment in task.comment_set.all %}
    {{ comment }}{% endfor %}

类似的,QuerySets提供了一个叫count()的方法用来计算所包含对象的总个数。因此你可以得到关联到task上的comment总和:

{{ task.comment_set.all.count }}

当然你可以轻松的访问你之前已经定义过的模型中的方法:

models.py:

class Task(models.Model):
    def foo(self):
        return "bar"

template.html:

{{ task.foo }}

由于Django有意限制了模板语言中的逻辑处理,你不可以在方法中传递参数,数据应该在view处理,再传入模板中展示。

自定义标签和过滤器库

一些应用提供了自定义标签和过滤器的库,要想在模板中使用,请确保该应用在INSTALLED_APPS(这里我添加了'django.contrib.humanize'库),然后用load标签来加载:

{% load humanize %}

{{ 45000|intcomma }}

在上面例子中,我们用load标签来加载humanize标签库,这样我们就可以使用该库中的intcomma过滤器了。如果你开启了django.contrib.admindocs,你可以在admin站查询文档然后找出你安装的自定义库列表。

load标签后面可以跟多个库名,当中用空格隔开:

{% load humanize i18n %}

查看Custom template tags and filters文章来获取更多关于自定义模板库的信息。

自定义库与模板继承

当你加载自定义标签或过滤器库时,这个库中的标签以及过滤器只能在当前模板使用,在其父模板或子模板是不起作用的。

比如,有一个模板foo.html写了{% load humanize %},其子模板中写了{ % extends “foo.html” %},但子模板还是无法访问父模板中humanize库中的标签和过滤器的,要想访问就必须在子模板中再次添加{% load humanize %}

这样的特性主要是为了项目的维护性和逻辑性。



作者:蛇发女妖
链接:https://www.jianshu.com/p/1664dcfd840c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。