1. 程式人生 > >Django模板語言詳解

Django模板語言詳解

本節將介紹Django模版系統的語法。Django模版語言致力於在效能和簡單性上取得平衡。

如果你有過其它程式設計背景,或者使用過一些在HTML中直接混入程式程式碼的語言,那麼你需要記住,Django的模版系統並不是簡單的將Python嵌入到HTML中。

一、模板

模版是純文字檔案,可以生成任何基於文字的檔案格式,比如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 %} 

二、變數

變數看起來就像是這樣: {{ variable }}。

當模版引擎遇到一個變數,它將從上下文context中獲取這個變數的值,然後用值替換掉它本身。

變數的命名包括任何字母數字以及下劃線("_")的組合。點(".")也有可能會在變數名中出現,不過它有特殊的含義。最重要的是,變數名稱中不能有空格或標點符號

當模版系統遇到點("."),它將以這樣的順序查詢這個圓點具體代表的功能:

  • 字典查詢(Dictionary lookup)
  • 屬性或方法查詢(Attribute or method lookup)
  • 數字索引查詢(Numeric index lookup)

如果你使用的變數不存在,模版系統將插入string_if_invalid選項的值,預設設定為''(空字串)。

注意,像{{ foo.bar }}這種模版表示式中的“bar”,如果在模版上下文中存在,將解釋為一個字面意義的字串而不是使用變數bar的值 。

三、過濾器

過濾器看起來是這樣的:{{ name|lower }}。使用管道符號(|)來應用過濾器。該過濾器將文字轉換成小寫。

過濾器可以“連結”。一個過濾器的輸出應用於下一個過濾器。例如:{{ text|escape|linebreaks }}就是一個常用的過濾器鏈,它首先轉移文字內容,然後把文字行轉成<p>標籤。

一些過濾器帶有引數。 過濾器的引數看起來像是這樣: {{ bio|truncatewords:30 }}。 這將顯示bio變數的前30個詞。

過濾器引數包含空格的話,必須用引號包起來。例如,使用逗號和空格去連線一個列表中的元素,你需要使用{{ list|join:", " }}

Django提供了大約六十個內建的模版過濾器,很多時候你想要的功能,它都已經提供了,經常檢視這些過濾器,發現新大陸吧。下面是一些常用的模版過濾器:

1. default

為false或者空變數提供預設值,像這樣:

{{ value|default:"nothing" }}

2. length

返回值的長度。它對字串和列表都起作用。

{{ value|length }}

如果value是['a', 'b', 'c', 'd'],那麼輸出4。

3. filesizeformat

格式化為“人類可讀”檔案大小單位(即'13 KB',4.1 MB','102 bytes'等)。

{{ value|filesizeformat }}

如果value是123456789,輸出將會是117.7MB。

我們可以建立自定義的模板過濾器和標籤,這是最終極的武器。

四、標籤

標籤看起來像是這樣的: {% tag %}

標籤比變數複雜得多,有些用於在輸出中建立文字,有些用於控制迴圈或判斷邏輯,有些用於載入外部資訊到模板中供以後的變數使用。

一些標籤需要開始和結束標籤(即 {% 標籤 %} ... 標籤 內容 ... {% ENDTAG %})。

Django自帶了大約24個內建的模版標籤。下面是一些常用的標籤:

1. for迴圈標籤

迴圈物件中每個元素。需要結束標籤{% endfor %} 。例如,顯示athlete_list中提供的運動員列表:

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

2. if,elif和else標籤

計算一個表示式,並且當表示式的值是“True”時,顯示塊中的內容。需要{% endif %}結束標籤。整體邏輯非常類似Python的if、elif和else,如下所示。:

{% 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 %}

在上面的例子中,如果athlete_list不是空的,運動員的數量將顯示為{{ athlete_list|length }}。否則,如果athlete_in_locker_room_list不為空,將顯示“Athletes should be out…”。如果兩個列表都是空的,將顯示“No athletes.” 。

還可以在if標籤中使用過濾器和多種運算子:

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

需要注意,大多數模版過濾器都返回字串型別,所以使用過濾器做整數型別的比較通常是錯誤的,但length是一個例外。

3. block和extends標籤

繼承和複寫模版。類似Python的類繼承和重寫機制。

五、註釋

要註釋模版中一行的部分內容,使用註釋語法:{# #}

例如,下面的模版將被渲染為'hello':

{# greeting #}hello

註釋可以包含任何模版內的程式碼,有效的或者無效的都可以。 像這樣:

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

以上是單行註釋(在{# .... #}中,不允許有新行)。

如果需要註釋掉模版中的多行內容,請使用comment標籤。

六、模板繼承

Django模版引擎中最強大也是最複雜的部分就是模版繼承了。模版繼承允許你建立一個包含基本“骨架”的父親模版,它包含站點中的共有元素,並且可以定義能夠被子模版覆蓋的blocks。

通過下面這個例子,理解模版繼承的概念:

<!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骨架。

“子模版”需要做的是先繼承父模板base.html,然後複寫、填充,或者說實現其中的blocks。

block是在子模版中可能會被覆蓋掉的位置。在上面的例子中,block標籤定義了三個可以被子模版內容填充的block,分別是title、content和siderbar。

再看下面的例子,子模版可能看起來是這樣的:

{% 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。 根據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,這種情況下,將使用父模版中的內容。父模版的{% block %}標籤中的內容總是被用作預設內容。

Django還支援多級繼承!常用方式是類似下面的三級結構:

  • 建立一個base.html模版,用來控制整個站點的主要視覺和體驗。
  • 為站點的每一個app,建立一個base_SECTIONNAME.html模版。 例如base_news.html,base_sports.html。這些模版都繼承base.html,並且包含了各自特有的樣式和設計。
  • 為每一個頁面型別,建立獨立的模版,例如新聞內容或者部落格文章。 這些模版繼承對應app的模版。

上面的方式可以使程式碼得到最大程度的複用,並且使得新增內容到共享的內容區域更加簡單,例如app範圍內的導航條。

下面是使用繼承的一些相關說明:

  • 如果在模版中使用{% extends %}標籤,它必須是模版中的第一個標籤,必須放在檔案首行!

  • 在base模版中設定越多的{% block %}標籤越好。子模版不必定義全部父模版中的blocks,所以可以在大多數blocks中填充合理的預設內容,然後,只定義你需要的那一個。多一點鉤子總比少一點好。

  • 如果發現你自己在複製大量重複的模版內容,那意味著你應該把重複的內容移動到父模版中的一個{% block %}中。

  • 如果需要獲取父模板中的block的內容,可以使用{{ block.super }}變數。如果想要在父block中新增內容而不是完全覆蓋它,這將非常有用。使用{{ block.super }} 插入的資料不會被自動轉義,因為父模板中的內容已經被轉義。

  • {% block %}之外建立的變數使用模板標籤的as語法,不能在塊內使用。

例如,下面的模板不會顯示任何內容:

{% trans "Title" as title %} {% block content %}{{ title }}{% endblock %}
  • 為了更好的可讀性,可以給{% endblock %}標籤一個取名字,像這樣:

    {% block content %} ...

在大型模版中,這有助於你清楚的看到哪一個{% block %}標籤被關閉了。

  • 最後,請注意不能在一個模版中定義多個相同名字的block標籤。

七、自動轉義HTML

當從模版中生成HTML檔案時,總會存在各種風險,比如xss程式碼注入等惡意攻擊。比如下面的模版片段:

Hello, {{ name }}

首先,它看起來像是無害的,用來顯示使用者的名字,但是設想一下,如果使用者像下面這樣輸入他的名字,會發生什麼:

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

使用這個名字的值,模版將會被渲染成這樣:

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

這意味著瀏覽器會彈出一個JavaScript警報框!

類似的,如果名字包含一個 '<' 符號(比如下面這樣),會發生什麼呢?

<b>username

這將會導致模版被渲染成這樣:

Hello, <b>username

這會導致網頁的其餘部分被粗體化!

顯然,使用者提交的資料都被不應該被盲目的信任,並且被直接插入到網頁中,因為一個懷有惡意的使用者可能會使用這樣的漏洞來做一些壞事。 這種型別的安全問題被叫做跨站指令碼攻擊(Cross Site Scripting)(XSS)。

為避免這個問題,有兩個選擇:

  • 第一,對每個不被信任的值執行escape過濾器,這將把潛在的有害的HTML字元轉換成無害的字串。在Django最初的幾年裡,這是預設的解決方案,但問題是它將責任放在開發人員/模板作者身上,以確保轉義了所有內容,而且很容易忘記轉義資料。
  • 第二,利用Django的自動HTML轉義功能。預設情況下,Django中的每個模板會自動轉義每個變數。也就是說,下面五個字元將被轉義:

    • <會轉換為&lt;
    • >會轉換為&gt;
    • '(單引號)轉換為&#39;
    • "(雙引號)會轉換為&quot;
    • &會轉換為&amp;

強烈建議:將第二種功能做為預設開啟的設定,不要關閉它!

但是,凡事都有正反兩面。有時,模板變數含有一些你打算渲染成原始HTML的資料,你並不想轉義這些內容。 例如,你可能會在資料庫中儲存一些HTML程式碼,並且直接在模板中嵌入它們。或者,你可能使用Django的模板系統來生成不是HTML的文字 -- 比如郵件資訊。要怎麼辦呢?

對於單個變數

使用safe過濾器來關閉變數上的自動轉義:

This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}

safe是safe from further escaping或者can be safely interpreted as HTML的縮寫。請確保你知道自己在用safe過濾器幹什麼!在上面的例子中,如果data含有<b>,輸出會是:

This will be escaped: &lt;b&gt; 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 %}

自動轉義標籤autoescape還會作用於擴充套件(extend)了當前模板的模板,以及通過include標籤包含的模板,就像所有block標籤那樣。 看下面的例子:

# base.html檔案

{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1> {% block content %} {% endblock %} {% endautoescape %} # child.html檔案 {% extends "base.html" %} {% block title %}This &amp; that{% endblock %} {% block content %}{{ greeting }}{% endblock %} 

由於自動轉義標籤在base模板中關閉,它也會在child模板中關閉,導致當greeting變數含有<b>Hello!</b>字串時,會渲染HTML。

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

過濾器的字串引數:

之前我們展示過,過濾器的引數可以是字串:

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

要注意,所有這種字串引數在插入模板時都不會進行任何自動轉義。原因是,模板的作者可以控制字串字面值的內容,所以可以確保在模板編寫時文字經過正確轉義。白話講,就是,你個程式設計師對自己傳遞的引數心裡要有數!

也即是說你應該這樣編寫:

{{ data|default:"3 &lt; 2" }}

而不是:

{{ data|default:"3 < 2" }} {# 錯誤的做法#}

八、方法呼叫

這部分內容,如果你掌握的極大提高你的模版語言能力。

大多數物件上的方法呼叫同樣可用於模板中。這意味著模板能夠訪問到的不僅僅是物件的屬性(比如欄位名稱)和檢視中傳入的變數,還可以執行物件的方法。 例如,Django ORM提供了“entry_set”語法用於查詢關聯到外來鍵的物件集合。 所以,如果模型“comment”有一個外來鍵關聯到模型“task”,可以根據task遍歷其所有的comments,像這樣:

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

與之類似,QuerySets提供了count()方法來計算含有物件的總數。因此,你可以像這樣獲取所有關於當前任務的評論總數:

{{ task.comment_set.all.count }}

當然,還可以訪問已經顯式定義在模型上的方法:

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

template.html

{{ task.foo }}

由於Django有意限制了模板語言中的處理邏輯,不能夠在模板中傳遞引數來呼叫方法。資料應該在檢視中處理,然後傳遞給模板用於展示。這點不同於Django的ORM操作。

九、多對多呼叫

對於如下的模型:

from django.db import models

# Create your models here.

class Student(models.Model): name = models.CharField(max_length=128) class Course(models.Model): name = models.CharField(max_length=128) students = models.ManyToManyField('Student') 

模型Course有一個多對多欄位指向Student模型。

正向查詢

假設編寫了一個如下的檢視:

def test(request):
    course = models.Course.objects.get(pk=1) return render(request, 'course.html', locals()) 

獲取了id為1的course物件,並將它傳遞給course.html模版,模版程式碼如下:

{% for student in course.students.all %}

<p>{{ student.name }}</p> {% endfor %} 

首先通過course.students.all,查尋到course物件關聯的students物件集,然後用for標籤迴圈它,獲取每個student物件,再用student模型的定義,訪問其各個欄位的屬性。

反向查詢

對於反向查詢,從student往course查,假設有如下的檢視:

def test2(request):
    student = models.Student.objects.get(pk=1) return render(request, 'student.html', locals()) 

獲取了id為1的student物件,並將它傳遞給student.html模版,模版程式碼如下:

{% for course in  student.course_set.all %} {{ course.name }} {% endfor %}

通過student.course_set.all,反向獲取到student例項對應的所有course物件,然後再for標籤迴圈每個course,呼叫course的各種欄位屬性。

對於外來鍵ForeignKey,其用法基本類似。只不過正向是obj.fk,且只有1個對像,不是集合。反向則是obj.fk_set,類似多對多。

十、使用自定義標籤和過濾器

某些應用提供了自定義的標籤和過濾器。想要在模板中使用它們,首先要確保該應用已經在INSTALLED_APPS 中(比如在下面的例子中,我們添加了'django.contrib.humanize'),之後在模板中使用load標籤:

{% load humanize %}
{{ 45000|intcomma }}

上面的例子中, load標籤載入了humanizeapp的標籤庫,之後我們可以使用它的intcomma過濾器。

如果你開啟了django.contrib.admindocs,可以查詢admin站點中的文件,檢視你安裝的自定義庫列表。

load標籤可以同時接受多個庫名稱,由空格分隔。 例如:

{% load humanize i18n %}

自定義庫和模板繼承:

當你載入一個自定義標籤或過濾器庫時,標籤或過濾器只在當前模板中有效--並不是帶有模板繼承關係的任何父模板或者子模版中都有效。白話說就是,你在父模板中可能載入了自定義標籤,然並卵,你在子模版中還要再載入一次!

例如,如果一個模板foo.html帶有{% load humanize %},子模版(例如,帶有{% extends "foo.html" %})中不能訪問humanize模板標籤和過濾器。 子模版需要再新增自己的{% load humanize %}

這個特性是出於保持可維護性和邏輯性的目的。