如何建立自訂樣板標籤與篩選器

Django 的樣板語言帶有多種內建標籤與篩選器,旨在滿足應用程式的呈現邏輯需求。儘管如此,您可能會發現自己需要核心樣板原語組未涵蓋的功能。您可以使用 Python 定義自訂標籤與篩選器來擴展樣板引擎,然後使用 {% load %} 標籤將其提供給您的樣板。

程式碼佈局

指定自訂樣板標籤與篩選器最常見的位置是在 Django 應用程式內部。如果它們與現有的應用程式相關,則將它們捆綁在那裡是有意義的;否則,它們可以新增到一個新的應用程式中。當 Django 應用程式新增到 INSTALLED_APPS 時,它在下面描述的傳統位置中定義的任何標籤都會自動提供以在樣板中載入。

應用程式應包含一個 templatetags 目錄,與 models.pyviews.py 等位於同一層級。如果它尚不存在,請建立它 - 不要忘記 __init__.py 檔案以確保該目錄被視為 Python 套件。

開發伺服器不會自動重新啟動

新增 templatetags 模組後,您需要重新啟動伺服器,才能在樣板中使用標籤或篩選器。

您的自訂標籤與篩選器將存在於 templatetags 目錄內部的模組中。模組檔案的名稱是您稍後載入標籤時將使用的名稱,因此請小心選擇一個不會與另一個應用程式中的自訂標籤與篩選器衝突的名稱。

例如,如果您的自訂標籤/篩選器位於名為 poll_extras.py 的檔案中,您的應用程式佈局可能如下所示

polls/
    __init__.py
    models.py
    templatetags/
        __init__.py
        poll_extras.py
    views.py

在您的樣板中,您將使用以下內容

{% load poll_extras %}

包含自訂標籤的應用程式必須在 INSTALLED_APPS 中,才能使 {% load %} 標籤生效。這是一項安全性功能:它允許您在單一主機上託管多個樣板庫的 Python 程式碼,而無需為每個 Django 安裝啟用對所有程式碼的存取權。

您可以將任意數量的模組放入 templatetags 套件中。請記住,{% load %} 陳述式將載入給定 Python 模組名稱(而非應用程式名稱)的標籤/篩選器。

若要成為有效的標籤庫,模組必須包含一個名為 register 的模組層級變數,該變數是 template.Library 執行個體,其中註冊了所有標籤與篩選器。因此,在您的模組頂部附近,放入以下內容

from django import template

register = template.Library()

或者,可以透過 DjangoTemplates'libraries' 引數註冊樣板標籤模組。如果您想要在使用樣板標籤時使用與樣板標籤模組名稱不同的標籤,這會很有用。它還允許您在不安裝應用程式的情況下註冊標籤。

幕後花絮

如需大量範例,請閱讀 Django 預設篩選器與標籤的原始碼。它們分別位於 django/template/defaultfilters.pydjango/template/defaulttags.py 中。

如需有關 load 標籤的詳細資訊,請閱讀其文件。

撰寫自訂樣板篩選器

自訂篩選器是接受一或兩個引數的 Python 函式

  • 變數的值 (輸入) – 不一定是字串。

  • 引數的值 – 這可以有預設值,也可以完全省略。

例如,在篩選器 {{ var|foo:"bar" }} 中,篩選器 foo 將被傳遞變數 var 和引數 "bar"

由於樣板語言不提供例外處理,因此從樣板篩選器引發的任何例外都會顯示為伺服器錯誤。因此,如果存在要傳回的合理後援值,篩選函式應避免引發例外。如果輸入代表樣板中的明顯錯誤,則引發例外可能仍然比隱藏錯誤的無聲失敗更好。

以下是一個篩選器定義範例

def cut(value, arg):
    """Removes all values of arg from the given string"""
    return value.replace(arg, "")

以下是如何使用該篩選器的範例

{{ somevariable|cut:"0" }}

大多數篩選器不接受引數。在這種情況下,請將引數從函式中排除

def lower(value):  # Only one argument.
    """Converts a string into all lowercase"""
    return value.lower()

註冊自訂篩選器

django.template.Library.filter()

撰寫篩選器定義後,您需要將其註冊到您的 Library 執行個體,使其可供 Django 的樣板語言使用

register.filter("cut", cut)
register.filter("lower", lower)

Library.filter() 方法接受兩個引數

  1. 篩選器的名稱 – 字串。

  2. 編譯函式 – Python 函式 (而不是作為字串的函式名稱)。

您可以改用 register.filter() 作為裝飾器

@register.filter(name="cut")
def cut(value, arg):
    return value.replace(arg, "")


@register.filter
def lower(value):
    return value.lower()

如果您省略 name 引數,如上面的第二個範例所示,Django 將使用函式的名稱作為篩選器名稱。

最後,register.filter() 也接受三個關鍵字引數 is_safeneeds_autoescapeexpects_localtime。這些引數在下面的 篩選器和自動跳脫篩選器和時區 中說明。

預期字串的樣板篩選器

django.template.defaultfilters.stringfilter()

如果您正在撰寫僅預期字串作為第一個引數的樣板篩選器,則應使用裝飾器 stringfilter。這會將物件轉換為字串值,然後再傳遞到您的函式

from django import template
from django.template.defaultfilters import stringfilter

register = template.Library()


@register.filter
@stringfilter
def lower(value):
    return value.lower()

這樣,您就可以將整數傳遞給此篩選器,而不會導致 AttributeError (因為整數沒有 lower() 方法)。

篩選器和自動跳脫

撰寫自訂篩選器時,請考慮篩選器如何與 Django 的自動跳脫行為互動。請注意,有兩種字串類型可以在樣板程式碼內傳遞

  • 原始字串是原生 Python 字串。在輸出時,如果自動跳脫生效,則會進行跳脫,否則會保持不變。

  • 安全字串是已標記為在輸出時可安全地進行進一步跳脫的字串。任何必要的跳脫都已完成。它們通常用於包含要按原樣在用戶端上解譯的原始 HTML 的輸出。

    在內部,這些字串的類型為 SafeString。您可以使用類似以下的程式碼測試它們

    from django.utils.safestring import SafeString
    
    if isinstance(value, SafeString):
        # Do something with the "safe" string.
        ...
    

樣板篩選器程式碼分為以下兩種情況

  1. 您的篩選器不會在結果中引入任何 HTML 不安全的字元 (<>'"&),除非這些字元原本就已存在。在這種情況下,您可以讓 Django 為您處理所有的自動跳脫處理。您只需要在註冊您的篩選器函式時,將 is_safe 旗標設定為 True,如下所示

    @register.filter(is_safe=True)
    def myfilter(value):
        return value
    

    這個旗標告訴 Django,如果將一個「安全」的字串傳遞到您的篩選器中,結果仍然會是「安全」的,如果傳入的是不安全的字串,Django 會在必要時自動跳脫它。

    您可以將這理解為「這個篩選器是安全的 - 它不會引入任何不安全的 HTML 的可能性」。

    is_safe 是必要的原因是,有許多正常的字串操作會將 SafeData 物件轉回普通的 str 物件,與其試圖捕捉所有這些情況 (這會非常困難),不如讓 Django 在篩選器完成後修復損壞。

    例如,假設您有一個篩選器,會在任何輸入的結尾加上字串 xx。由於這不會在結果中引入任何危險的 HTML 字元(除了任何已經存在的字元之外),您應該用 is_safe 標記您的篩選器

    @register.filter(is_safe=True)
    def add_xx(value):
        return "%sxx" % value
    

    當這個篩選器在啟用自動跳脫的範本中使用時,只要輸入尚未標記為「安全」,Django 就會跳脫輸出。

    預設情況下,is_safeFalse,您可以從任何不需要它的篩選器中省略它。

    當您決定您的篩選器是否真的讓安全的字串保持安全時,請務必小心。如果您正在移除字元,您可能會在結果中不小心留下不平衡的 HTML 標籤或實體。例如,從輸入中移除 > 可能會將 <a> 變成 <a,這需要在輸出時跳脫,以避免造成問題。同樣地,移除分號 (;) 可以將 &amp; 變成 &amp,這不再是有效的實體,因此需要進一步的跳脫。大多數情況不會這麼棘手,但在檢查程式碼時,請留意任何類似的問題。

    將篩選器標記為 is_safe 會將篩選器的傳回值強制轉換為字串。如果您的篩選器應該傳回布林值或其他非字串值,將其標記為 is_safe 可能會產生意想不到的後果(例如,將布林值 False 轉換為字串 'False')。

  2. 或者,您的篩選器程式碼可以手動處理任何必要的跳脫。當您在結果中引入新的 HTML 標記時,這是必要的。您想要將輸出標記為安全,避免進一步的跳脫,以便您的 HTML 標記不會被進一步跳脫,因此您需要自己處理輸入。

    要將輸出標記為安全的字串,請使用 django.utils.safestring.mark_safe()

    但請小心。您需要做的不僅僅是將輸出標記為安全。您需要確保它真的安全的,而您要做的事情取決於自動跳脫是否生效。我們的想法是編寫可以在自動跳脫開啟或關閉的範本中運作的篩選器,以便讓您的範本作者更容易使用。

    為了讓您的篩選器知道目前的自動跳脫狀態,請在註冊您的篩選器函式時,將 needs_autoescape 旗標設定為 True。(如果您沒有指定此旗標,它預設為 False)。此旗標告訴 Django,您的篩選器函式想要傳遞一個額外的關鍵字引數,稱為 autoescape,如果自動跳脫生效,則為 True,否則為 False。建議將 autoescape 參數的預設值設定為 True,這樣如果您從 Python 程式碼呼叫該函式,它預設會啟用跳脫。

    例如,讓我們編寫一個篩選器,它會強調字串的第一個字元

    from django import template
    from django.utils.html import conditional_escape
    from django.utils.safestring import mark_safe
    
    register = template.Library()
    
    
    @register.filter(needs_autoescape=True)
    def initial_letter_filter(text, autoescape=True):
        first, other = text[0], text[1:]
        if autoescape:
            esc = conditional_escape
        else:
            esc = lambda x: x
        result = "<strong>%s</strong>%s" % (esc(first), esc(other))
        return mark_safe(result)
    

    needs_autoescape 旗標和 autoescape 關鍵字引數表示我們的函式會在篩選器被呼叫時知道自動跳脫是否生效。我們使用 autoescape 來決定是否需要將輸入資料傳遞給 django.utils.html.conditional_escape。(在後一種情況下,我們將恆等函式用作「跳脫」函式。)conditional_escape() 函式與 escape() 類似,只是它只會跳脫不是 SafeData 實例的輸入。如果將 SafeData 實例傳遞給 conditional_escape(),則資料會保持不變地傳回。

    最後,在上面的範例中,我們記得將結果標記為安全,以便我們的 HTML 直接插入到範本中,而無需進一步跳脫。

    在這種情況下,無需擔心 is_safe 旗標(雖然包含它不會造成任何損害)。當您手動處理自動跳脫問題並傳回安全字串時,無論如何,is_safe 旗標都不會改變任何事情。

警告

在重複使用內建篩選器時避免 XSS 漏洞

Django 的內建篩選器預設具有 autoescape=True,以便獲得正確的自動跳脫行為並避免跨網站指令碼漏洞。

在舊版本的 Django 中,重複使用 Django 的內建篩選器時請小心,因為 autoescape 預設為 None。您需要傳遞 autoescape=True 才能取得自動跳脫。

例如,如果您想要編寫一個名為 urlize_and_linebreaks 的自訂篩選器,將 urlizelinebreaksbr 篩選器組合在一起,則篩選器將如下所示

from django.template.defaultfilters import linebreaksbr, urlize


@register.filter(needs_autoescape=True)
def urlize_and_linebreaks(text, autoescape=True):
    return linebreaksbr(urlize(text, autoescape=autoescape), autoescape=autoescape)

然後

{{ comment|urlize_and_linebreaks }}

將等同於

{{ comment|urlize|linebreaksbr }}

篩選器和時區

如果您編寫一個對 datetime 物件進行操作的自訂篩選器,您通常會將 expects_localtime 旗標設定為 True 來註冊它

@register.filter(expects_localtime=True)
def businesshours(value):
    try:
        return 9 <= value.hour < 17
    except AttributeError:
        return ""

當設定此旗標時,如果篩選器的第一個引數是時區感知日期時間,Django 會在適當的情況下,根據 範本中時區轉換的規則,將其轉換為目前時區,然後再將其傳遞給您的篩選器。

編寫自訂範本標籤

標籤比篩選器更複雜,因為標籤可以做任何事情。Django 提供了一些捷徑,讓編寫大多數類型的標籤更容易。首先,我們將探索這些捷徑,然後說明如何在捷徑不夠強大時從頭開始編寫標籤。

簡單標籤

django.template.Library.simple_tag()

許多範本標籤會接受一些引數(字串或範本變數),並在僅基於輸入引數和一些外部資訊進行一些處理後傳回結果。例如,current_time 標籤可能會接受格式字串並根據格式傳回時間字串。

為了簡化這些類型標籤的建立,Django 提供了一個輔助函式 simple_tag。此函式是 django.template.Library 的方法,它會接受一個接受任意數量引數的函式,將其包裝在 render 函式和上述的其他必要位中,並將其註冊到範本系統。

我們的 current_time 函式可以這樣寫

import datetime
from django import template

register = template.Library()


@register.simple_tag
def current_time(format_string):
    return datetime.datetime.now().strftime(format_string)

關於 simple_tag 輔助函式,有幾點需要注意

  • 在呼叫我們的函式時,已經完成了檢查所需引數數量等操作,因此我們無需執行此操作。

  • 引數周圍的引號(如果有的話)已經被移除,因此我們會收到一個純字串。

  • 如果引數是範本變數,則會將變數的目前值傳遞給我們的函式,而不是變數本身。

與其他標籤公用程式不同,如果範本內容處於自動跳脫模式,simple_tag 會透過 conditional_escape() 傳遞其輸出,以確保正確的 HTML 並保護您免受 XSS 漏洞的侵害。

如果不希望額外跳脫處理,而且你完全確定你的程式碼不包含 XSS 漏洞,你需要使用 mark_safe()。若要建構小的 HTML 片段,強烈建議使用 format_html() 來取代 mark_safe()

如果你的樣板標籤需要存取目前的 context,你可以在註冊標籤時使用 takes_context 參數

@register.simple_tag(takes_context=True)
def current_time(context, format_string):
    timezone = context["timezone"]
    return your_get_current_time_method(timezone, format_string)

請注意,第一個參數必須命名為 context

關於 takes_context 選項如何運作的更多資訊,請參閱關於 包含標籤 的章節。

如果你需要重新命名你的標籤,你可以為它提供一個自訂的名稱

register.simple_tag(lambda x: x - 1, name="minusone")


@register.simple_tag(name="minustwo")
def some_function(value):
    return value - 2

simple_tag 函式可以接受任意數量的位置或關鍵字引數。例如

@register.simple_tag
def my_tag(a, b, *args, **kwargs):
    warning = kwargs["warning"]
    profile = kwargs["profile"]
    ...
    return ...

然後,在樣板中,任何數量的引數都可以透過空格分隔傳遞給樣板標籤。就像在 Python 中一樣,關鍵字引數的值是使用等號 (”=”) 設定的,而且必須在位置引數之後提供。例如

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

可以將標籤結果儲存在樣板變數中,而不是直接輸出它。這可以透過使用 as 引數,後面跟著變數名稱來完成。這樣做可以讓你在你認為合適的地方自行輸出內容

{% current_time "%Y-%m-%d %I:%M %p" as the_time %}
<p>The time is {{ the_time }}.</p>

包含標籤

django.template.Library.inclusion_tag()

另一種常見的樣板標籤類型是透過渲染另一個樣板來顯示一些資料的類型。例如,Django 的管理介面使用自訂的樣板標籤來顯示「新增/變更」表單頁面底部的按鈕。這些按鈕看起來總是相同的,但是連結目標會根據正在編輯的物件而改變 - 因此它們是使用從目前物件填入細節的小樣板的完美案例。(在管理介面中,這是 submit_row 標籤。)

這些類型的標籤稱為「包含標籤」。

撰寫包含標籤可能最好透過範例來示範。讓我們撰寫一個標籤,該標籤輸出給定 Poll 物件的選項列表,例如在 教學 中建立的物件。我們將像這樣使用該標籤

{% show_results poll %}

... 並且輸出會像這樣

<ul>
  <li>First choice</li>
  <li>Second choice</li>
  <li>Third choice</li>
</ul>

首先,定義一個函式,該函式接受引數並產生結果的資料字典。這裡的重點是我們只需要傳回一個字典,而不需要更複雜的東西。這將用作樣板片段的樣板 context。範例

def show_results(poll):
    choices = poll.choice_set.all()
    return {"choices": choices}

接下來,建立用來渲染標籤輸出的樣板。此樣板是標籤的固定功能:標籤撰寫者指定它,而不是樣板設計師。在我們的範例中,樣板非常短

<ul>
{% for choice in choices %}
    <li> {{ choice }} </li>
{% endfor %}
</ul>

現在,透過在 Library 物件上呼叫 inclusion_tag() 方法來建立並註冊包含標籤。在我們的範例中,如果上述樣板位於一個名為 results.html 的檔案中,而該檔案位於樣板載入器搜尋的目錄中,我們將像這樣註冊該標籤

# Here, register is a django.template.Library instance, as before
@register.inclusion_tag("results.html")
def show_results(poll): ...

或者,可以使用 django.template.Template 實例來註冊包含標籤

from django.template.loader import get_template

t = get_template("results.html")
register.inclusion_tag(t)(show_results)

...在第一次建立函式時。

有時,您的包含標籤可能需要大量的引數,這會讓樣板作者很痛苦,他們需要傳入所有引數並記住它們的順序。為了解決這個問題,Django 為包含標籤提供了一個 takes_context 選項。如果您在建立樣板標籤時指定 takes_context,則該標籤將不需要任何引數,而且底層 Python 函式將會有一個引數 - 該引數為呼叫標籤時的樣板 context。

例如,假設您正在撰寫一個包含標籤,該標籤將始終在包含指向主頁面的 home_linkhome_title 變數的 context 中使用。以下是 Python 函式的外觀

@register.inclusion_tag("link.html", takes_context=True)
def jump_link(context):
    return {
        "link": context["home_link"],
        "title": context["home_title"],
    }

請注意,該函式的第一個參數必須命名為 context

register.inclusion_tag() 行中,我們指定了 takes_context=True 和樣板的名稱。以下是樣板 link.html 的外觀

Jump directly to <a href="{{ link }}">{{ title }}</a>.

然後,每當您要使用該自訂標籤時,載入它的程式庫,並在不帶任何引數的情況下呼叫它,就像這樣

{% jump_link %}

請注意,當您使用 takes_context=True 時,不需要將引數傳遞給樣板標籤。它會自動存取 context。

takes_context 參數預設為 False。當它設定為 True 時,標籤會傳遞 context 物件,如本範例所示。這是此案例與先前 inclusion_tag 範例之間唯一的區別。

inclusion_tag 函式可以接受任意數量的位置或關鍵字引數。例如

@register.inclusion_tag("my_template.html")
def my_tag(a, b, *args, **kwargs):
    warning = kwargs["warning"]
    profile = kwargs["profile"]
    ...
    return ...

然後,在樣板中,任何數量的引數都可以透過空格分隔傳遞給樣板標籤。就像在 Python 中一樣,關鍵字引數的值是使用等號 (”=”) 設定的,而且必須在位置引數之後提供。例如

{% my_tag 123 "abcd" book.title warning=message|lower profile=user.profile %}

進階自訂樣板標籤

有時,用於建立自訂樣板標籤的基本功能不足。別擔心,Django 讓您可以完全存取從頭開始建構樣板標籤所需的內部元件。

快速概觀

樣板系統以兩個步驟進行:編譯和渲染。要定義自訂樣板標籤,您需要指定編譯如何運作以及渲染如何運作。

當 Django 編譯樣板時,它會將原始樣板文字分割成「節點」。每個節點都是 django.template.Node 的一個實例,並具有 render() 方法。已編譯的樣板是 Node 物件的列表。當您在已編譯的樣板物件上呼叫 render() 時,該樣板會針對其節點列表中的每個 Node 呼叫 render(),並帶有指定的 context。將結果全部串連在一起,形成樣板的輸出。

因此,要定義自訂樣板標籤,您需要指定如何將原始樣板標籤轉換為 Node (編譯函式),以及節點的 render() 方法的作用。

撰寫編譯函式

對於樣板解析器遇到的每個樣板標籤,它都會使用標籤內容和解析器物件本身呼叫 Python 函式。此函式負責根據標籤的內容傳回 Node 實例。

例如,讓我們撰寫樣板標籤 {% current_time %} 的完整實作,該標籤會根據標籤中給定的參數,以 strftime() 語法顯示目前日期/時間。在其他任何事情之前,決定標籤語法是個好主意。在我們的例子中,假設標籤應該像這樣使用

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>

此函式的解析器應該抓取參數並建立 Node 物件

from django import template


def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires a single argument" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode(format_string[1:-1])

注意事項

  • parser 是樣板解析器物件。在此範例中,我們不需要它。

  • token.contents 是標籤的原始內容的字串。在我們的範例中,它是 'current_time "%Y-%m-%d %I:%M %p"'

  • token.split_contents() 方法會以空格分隔引數,同時將引號字串保留在一起。比較直接的 token.contents.split() 並不會那麼穩健,因為它會很直接地在所有空格上分割,包括引號字串內的空格。始終使用 token.split_contents() 是個好主意。

  • 此函式負責針對任何語法錯誤引發 django.template.TemplateSyntaxError,並提供有用的訊息。

  • TemplateSyntaxError 例外狀況使用 tag_name 變數。請勿在您的錯誤訊息中硬式編碼標籤的名稱,因為這會將標籤的名稱與您的函式耦合。token.contents.split()[0] 「永遠」是您的標籤名稱 - 即使標籤沒有引數。

  • 該函式會傳回 CurrentTimeNode,其中包含節點需要知道的有關此標籤的所有資訊。在此範例中,它會傳遞引數 – "%Y-%m-%d %I:%M %p"。樣板標籤的前後引號會在 format_string[1:-1] 中移除。

  • 解析是非常底層的。Django 開發者曾經嘗試在該解析系統之上編寫小型框架,使用 EBNF 文法等技術,但這些實驗使模板引擎變得太慢。它之所以底層是因為這樣最快。

編寫渲染器

編寫自訂標籤的第二步是定義一個具有 render() 方法的 Node 子類別。

延續上面的範例,我們需要定義 CurrentTimeNode

import datetime
from django import template


class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        return datetime.datetime.now().strftime(self.format_string)

注意事項

  • __init__()do_current_time() 取得 format_string。始終透過 Node__init__() 將任何選項/參數/引數傳遞給它。

  • render() 方法是實際執行工作的地方。

  • render() 通常應以靜默方式失敗,尤其是在生產環境中。但在某些情況下,特別是如果 context.template.engine.debugTrue,此方法可能會引發例外狀況,以方便除錯。例如,如果幾個核心標籤收到錯誤的引數數量或類型,則會引發 django.template.TemplateSyntaxError

最終,編譯和渲染的分離會產生一個有效率的模板系統,因為一個模板可以渲染多個上下文,而無需多次解析。

自動跳脫的考量

模板標籤的輸出不會自動執行自動跳脫篩選器(除了上面描述的 simple_tag() 之外)。但是,在編寫模板標籤時,您仍然應該記住一些事項。

如果您的模板標籤的 render() 方法將結果儲存在上下文變數中(而不是在字串中傳回結果),則應在適當的情況下呼叫 mark_safe()。當變數最終被渲染時,它會受到當時有效的自動跳脫設定的影響,因此應將應避免進一步跳脫的內容標記為如此。

此外,如果您的模板標籤建立一個新的上下文來執行某些子渲染,請將自動跳脫屬性設定為目前上下文的值。Context 類別的 __init__ 方法接受一個名為 autoescape 的參數,您可以用於此目的。例如

from django.template import Context


def render(self, context):
    # ...
    new_context = Context({"var": obj}, autoescape=context.autoescape)
    # ... Do something with new_context ...

這不是一個很常見的情況,但如果您自己渲染模板,它會很有用。例如

def render(self, context):
    t = context.template.engine.get_template("small_fragment.html")
    return t.render(Context({"var": obj}, autoescape=context.autoescape))

如果我們在這個範例中忽略將目前的 context.autoescape 值傳遞給我們新的 Context,則結果將始終自動跳脫,如果模板標籤在 {% autoescape off %} 區塊內使用,這可能不是所需的行為。

執行緒安全考量

一旦解析節點,其 render 方法可能會被呼叫任意次數。由於 Django 有時會在多執行緒環境中執行,因此單一節點可能會同時使用不同的上下文進行渲染,以回應兩個不同的請求。因此,請務必確保您的模板標籤是執行緒安全的。

為了確保您的模板標籤是執行緒安全的,您永遠不應該在節點本身上儲存狀態資訊。例如,Django 提供了一個內建的 cycle 模板標籤,該標籤會在每次渲染時在給定字串清單之間循環。

{% for o in some_list %}
    <tr class="{% cycle 'row1' 'row2' %}">
        ...
    </tr>
{% endfor %}

CycleNode 的簡單實作可能如下所示

import itertools
from django import template


class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cycle_iter = itertools.cycle(cyclevars)

    def render(self, context):
        return next(self.cycle_iter)

但是,假設我們有兩個模板同時渲染上面的模板程式碼片段

  1. 執行緒 1 執行其第一次迴圈迭代,CycleNode.render() 傳回 'row1'

  2. 執行緒 2 執行其第一次迴圈迭代,CycleNode.render() 傳回 'row2'

  3. 執行緒 1 執行其第二次迴圈迭代,CycleNode.render() 傳回 'row1'

  4. 執行緒 2 執行其第二次迴圈迭代,CycleNode.render() 傳回 'row2'

CycleNode 正在迭代,但它是全域迭代。就執行緒 1 和執行緒 2 而言,它始終傳回相同的值。這不是我們想要的!

為了解決這個問題,Django 提供了一個 render_context,它與目前正在渲染的模板的 context 相關聯。render_context 的行為類似於 Python 字典,應該用於在 render 方法的呼叫之間儲存 Node 狀態。

讓我們重構我們的 CycleNode 實作以使用 render_context

class CycleNode(template.Node):
    def __init__(self, cyclevars):
        self.cyclevars = cyclevars

    def render(self, context):
        if self not in context.render_context:
            context.render_context[self] = itertools.cycle(self.cyclevars)
        cycle_iter = context.render_context[self]
        return next(cycle_iter)

請注意,將在 Node 的生命週期中不會變更的全域資訊儲存為屬性是完全安全的。以 CycleNode 為例,cyclevars 引數在 Node 實例化之後不會變更,因此我們不需要將其放在 render_context 中。但是特定於目前正在渲染的模板的狀態資訊(例如 CycleNode 的目前迭代)應儲存在 render_context 中。

注意

請注意我們如何使用 self 來限定 render_context 內的 CycleNode 特定資訊。給定模板中可能有多個 CycleNodes,因此我們需要小心不要覆蓋另一個節點的狀態資訊。執行此操作最簡單的方法是始終使用 self 作為 render_context 的鍵。如果您要追蹤多個狀態變數,請將 render_context[self] 作為字典。

註冊標籤

最後,使用您模組的 Library 實例註冊標籤,如上面的編寫自訂模板標籤中所述。範例

register.tag("current_time", do_current_time)

tag() 方法接受兩個引數

  1. 模板標籤的名稱 - 字串。如果省略此項,將使用編譯函式的名稱。

  2. 編譯函式 – Python 函式 (而不是作為字串的函式名稱)。

與篩選器註冊一樣,也可以將此用作裝飾器

@register.tag(name="current_time")
def do_current_time(parser, token): ...


@register.tag
def shout(parser, token): ...

如果您省略 name 引數,如上面的第二個範例所示,Django 會將函式的名稱用作標籤名稱。

將模板變數傳遞給標籤

雖然您可以使用 token.split_contents() 將任意數量的引數傳遞給模板標籤,但這些引數都會解壓縮為字串文字。若要將動態內容(模板變數)作為引數傳遞給模板標籤,則需要更多工作。

雖然之前的範例已將目前時間格式化為字串並傳回該字串,但假設您想從物件中傳遞 DateTimeField 並讓模板標籤格式化該日期時間

<p>This post was last updated at {% format_time blog_entry.date_updated "%Y-%m-%d %I:%M %p" %}.</p>

最初,token.split_contents() 將傳回三個值

  1. 標籤名稱 format_time

  2. 字串 'blog_entry.date_updated'(不含周圍的引號)。

  3. 格式化字串 '"%Y-%m-%d %I:%M %p"'split_contents() 的傳回值將包含此類字串文字的前導和尾隨引號。

現在您的標籤應該開始如下所示

from django import template


def do_format_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, date_to_be_formatted, format_string = token.split_contents()
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires exactly two arguments" % token.contents.split()[0]
        )
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return FormatTimeNode(date_to_be_formatted, format_string[1:-1])

您還必須變更渲染器,以檢索 blog_entry 物件的 date_updated 屬性的實際內容。這可以使用 django.template 中的 Variable() 類別來完成。

若要使用 Variable 類別,請使用要解析的變數名稱來實例化它,然後呼叫 variable.resolve(context)。例如:

class FormatTimeNode(template.Node):
    def __init__(self, date_to_be_formatted, format_string):
        self.date_to_be_formatted = template.Variable(date_to_be_formatted)
        self.format_string = format_string

    def render(self, context):
        try:
            actual_date = self.date_to_be_formatted.resolve(context)
            return actual_date.strftime(self.format_string)
        except template.VariableDoesNotExist:
            return ""

如果在頁面的目前內容中無法解析傳遞給它的字串,變數解析會拋出 VariableDoesNotExist 例外。

在內容中設定變數

上面的範例會輸出一個值。一般而言,如果您的範本標籤設定範本變數而不是輸出值,會更具彈性。這樣,範本作者就可以重複使用您的範本標籤所建立的值。

若要在內容中設定變數,請在 render() 方法中對內容物件使用字典賦值。以下是 CurrentTimeNode 的更新版本,它會設定範本變數 current_time,而不是輸出它。

import datetime
from django import template


class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = format_string

    def render(self, context):
        context["current_time"] = datetime.datetime.now().strftime(self.format_string)
        return ""

請注意,render() 會傳回空字串。render() 應該始終傳回字串輸出。如果範本標籤的所有動作只是設定變數,則 render() 應該傳回空字串。

以下是如何使用此標籤新版本的方法:

{% current_time "%Y-%m-%d %I:%M %p" %}<p>The time is {{ current_time }}.</p>

內容中的變數範圍

在內容中設定的任何變數都只會在範本中被賦值的同一個 block 中可用。此行為是有意的;它為變數提供範圍,以便它們不會與其他區塊中的內容衝突。

但是,CurrentTimeNode2 有一個問題:變數名稱 current_time 是硬式編碼的。這表示您需要確保您的範本在其他任何地方都未使用 {{ current_time }},因為 {% current_time %} 會盲目地覆寫該變數的值。更簡潔的解決方案是讓範本標籤指定輸出變數的名稱,如下所示:

{% current_time "%Y-%m-%d %I:%M %p" as my_current_time %}
<p>The current time is {{ my_current_time }}.</p>

若要執行此操作,您需要重構編譯函數和 Node 類別,如下所示:

import re


class CurrentTimeNode3(template.Node):
    def __init__(self, format_string, var_name):
        self.format_string = format_string
        self.var_name = var_name

    def render(self, context):
        context[self.var_name] = datetime.datetime.now().strftime(self.format_string)
        return ""


def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        raise template.TemplateSyntaxError(
            "%r tag requires arguments" % token.contents.split()[0]
        )
    m = re.search(r"(.*?) as (\w+)", arg)
    if not m:
        raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name)
    format_string, var_name = m.groups()
    if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")):
        raise template.TemplateSyntaxError(
            "%r tag's argument should be in quotes" % tag_name
        )
    return CurrentTimeNode3(format_string[1:-1], var_name)

此處的不同之處在於 do_current_time() 會抓取格式字串和變數名稱,並將兩者都傳遞給 CurrentTimeNode3

最後,如果您只需要為自訂的內容更新範本標籤使用簡單的語法,請考慮使用 simple_tag() 捷徑,它支援將標籤結果指派給範本變數。

剖析直到另一個區塊標籤

範本標籤可以協同運作。例如,標準的 {% comment %} 標籤會隱藏所有內容,直到 {% endcomment %} 為止。若要建立類似此範例的範本標籤,請在編譯函數中使用 parser.parse()

以下說明如何實作簡化的 {% comment %} 標籤:

def do_comment(parser, token):
    nodelist = parser.parse(("endcomment",))
    parser.delete_first_token()
    return CommentNode()


class CommentNode(template.Node):
    def render(self, context):
        return ""

注意

{% comment %} 的實際實作略有不同,因為它允許損毀的範本標籤出現在 {% comment %}{% endcomment %} 之間。它是藉由呼叫 parser.skip_past('endcomment') 而不是 parser.parse(('endcomment',)),然後接著呼叫 parser.delete_first_token() 來達成此目的,因此避免了產生節點清單。

parser.parse() 會接收區塊標籤的名稱的元組,直到解析為止。它會傳回 django.template.NodeList 的執行個體,這是剖析器在遇到元組中命名的任何標籤「之前」遇到的所有 Node 物件的清單。

在上述範例的 "nodelist = parser.parse(('endcomment',))" 中,nodelist{% comment %}{% endcomment %} 之間的所有節點的清單,不包括 {% comment %}{% endcomment %} 本身。

在呼叫 parser.parse() 之後,剖析器尚未「取用」{% endcomment %} 標籤,因此程式碼需要明確呼叫 parser.delete_first_token()

CommentNode.render() 會傳回空字串。任何在 {% comment %}{% endcomment %} 之間的任何內容都會被忽略。

剖析直到另一個區塊標籤並儲存內容

在先前的範例中,do_comment() 會捨棄 {% comment %}{% endcomment %} 之間的所有內容。實際上,您可以處理區塊標籤之間的程式碼。

例如,以下是一個自訂範本標籤 {% upper %},它會將自身與 {% endupper %} 之間的所有內容大寫。

用法

{% upper %}This will appear in uppercase, {{ your_name }}.{% endupper %}

如同先前的範例,我們會使用 parser.parse()。但這次,我們會將產生的 nodelist 傳遞給 Node

def do_upper(parser, token):
    nodelist = parser.parse(("endupper",))
    parser.delete_first_token()
    return UpperNode(nodelist)


class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist

    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()

此處唯一的新概念是 UpperNode.render() 中的 self.nodelist.render(context)

如需複雜呈現的更多範例,請參閱 {% for %}django/template/defaulttags.py{% if %}django/template/smartif.py 中的原始碼。

返回頂端