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中的每個模板會自動轉義每個變數。也就是說,下面五個字元將被轉義:
<
會轉換為<
>
會轉換為>
'
(單引號)轉換為'
"
(雙引號)會轉換為"
&
會轉換為&
強烈建議:將第二種功能做為預設開啟的設定,不要關閉它!
但是,凡事都有正反兩面。有時,模板變數含有一些你打算渲染成原始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: <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 %}
自動轉義標籤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 & that{% endblock %} {% block content %}{{ greeting }}{% endblock %}
由於自動轉義標籤在base模板中關閉,它也會在child模板中關閉,導致當greeting變數含有<b>Hello!</b>
字串時,會渲染HTML。
<h1>This & that</h1>
<b>Hello!</b>
過濾器的字串引數:
之前我們展示過,過濾器的引數可以是字串:
{{ data|default:"This is a string literal." }}
要注意,所有這種字串引數在插入模板時都不會進行任何自動轉義。原因是,模板的作者可以控制字串字面值的內容,所以可以確保在模板編寫時文字經過正確轉義。白話講,就是,你個程式設計師對自己傳遞的引數心裡要有數!
也即是說你應該這樣編寫:
{{ data|default:"3 < 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標籤載入了humanize
app的標籤庫,之後我們可以使用它的intcomma過濾器。
如果你開啟了django.contrib.admindocs
,可以查詢admin站點中的文件,檢視你安裝的自定義庫列表。
load標籤可以同時接受多個庫名稱,由空格分隔。 例如:
{% load humanize i18n %}
自定義庫和模板繼承:
當你載入一個自定義標籤或過濾器庫時,標籤或過濾器只在當前模板中有效--並不是帶有模板繼承關係的任何父模板或者子模版中都有效。白話說就是,你在父模板中可能載入了自定義標籤,然並卵,你在子模版中還要再載入一次!
例如,如果一個模板foo.html
帶有{% load humanize %}
,子模版(例如,帶有{% extends "foo.html" %}
)中不能訪問humanize模板標籤和過濾器。 子模版需要再新增自己的{% load humanize %}
。
這個特性是出於保持可維護性和邏輯性的目的。