如何创建自定义模板标记和过滤器

Django的模板语言有多种 built-in tags and filters 旨在满足应用程序的表示逻辑需求。然而,您可能会发现自己需要的功能并没有被核心模板原语集所覆盖。您可以通过使用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()

或者,模板标记模块可以通过 'libraries' 参数 DjangoTemplates . 如果要在加载模板标记时使用与模板标记模块名称不同的标签,则此选项非常有用。它还允许您在不安装应用程序的情况下注册标记。

幕后

有关大量的示例,请阅读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 . 这些参数在 filters and auto-escapingfilters and time zones 下面。

需要字符串的模板筛选器

django.template.defaultfilters.stringfilter()

如果编写的模板筛选器只希望使用字符串作为第一个参数,则应使用decorator 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的输出,这些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,如果将一个“safe”字符串传递到您的过滤器中,结果仍然是“safe”,并且如果传入了一个非safe字符串,则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() .

    不过要小心。您需要做的不仅仅是将输出标记为安全。你需要确保 is 安全,您所做的取决于自动转义是否有效。其思想是编写可以在自动转义处于打开或关闭状态的模板中操作的过滤器,以便使模板作者更轻松地进行操作。

    为了让您的过滤器知道当前的自动转义状态,请设置 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 或不. (In在后一种情况下,我们使用身份函数作为“逃逸”函数。)的 conditional_escape() 功能就像 escape() 除了它只逸出 not 一个 SafeData instance.如果 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将在将其传递给筛选器之前将其转换为当前时区,具体取决于 rules for time zones conversions in templates .

正在写入自定义模板标记

标签比过滤器更复杂,因为标签可以做任何事情。Django提供了许多快捷方式,使大多数类型的标记更容易编写。首先,我们将探讨这些快捷方式,然后解释如何在快捷方式不够强大时从头开始为这些情况编写标记。

简单标签

django.template.Library.simple_tag()

许多模板标记接受许多参数(字符串或模板变量),并在仅仅基于输入参数和一些外部信息进行一些处理之后返回结果。例如,A 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漏洞的攻击。

如果不需要额外的转义,则需要使用 mark_safe() 如果您确信您的代码不包含XSS漏洞。对于构建小的HTML片段,使用 format_html() 而不是 mark_safe() 强烈建议。

如果模板标记需要访问当前上下文,则可以使用 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)

注意第一个论点 must 被称为 context .

有关如何 takes_context 选项工程,参见 inclusion tags .

如果需要重命名标记,可以为其提供自定义名称:

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>

简单块标签

New in Django 5.2.
django.template.Library.simple_block_tag()

当渲染模板的一部分需要传递到自定义标签时,Django提供 simple_block_tag 帮助器功能来实现这一点。类似于 simple_tag() ,该函数接受自定义标记函数,但带有额外的 content 参数,其中包含标签内定义的渲染内容。这使得动态模板部分可以轻松地合并到自定义标签中。

例如,创建图表的自定义块标签可能看起来像这样::

from django import template
from myapp.charts import render_chart

register = template.Library()


@register.simple_block_tag
def chart(content):
    return render_chart(source=content)

content 参数包含介于 {% chart %}{% endchart %} 标签:

{% chart %}
  digraph G {
      label = "Chart for {{ request.user }}"
      A -> {B C}
  }
{% endchart %}

如果内部有其他模板标签或变量 content 块,它们将在传递给标签函数之前被渲染。在上面的例子中, request.user 到时候就会解决 render_chart 被称为。

区块标签以关闭 end{name} (for例如, endchart ).这可以使用 end_name 参数::

@register.simple_block_tag(end_name="endofchart")
def chart(content):
    return render_chart(source=content)

这需要这样的模板定义:

{% chart %}
  digraph G {
      label = "Chart for {{ request.user }}"
      A -> {B C}
  }
{% endofchart %}

需要注意的几件事 simple_block_tag :

  • 必须调用第一个参数 content ,并且它将包含模板标签的内容作为渲染字符串。

  • 传递给标记的变量不包括在内容的呈现上下文中, {% with %} 标签

就像 simple_tag , simple_block_tag :

  • 验证论点的数量和质量。

  • 如有必要,删除论点中的引言。

  • 相应地逃离输出。

  • 支持通过 takes_context=True 在注册时访问上下文。请注意,在这种情况下,自定义函数的第一个参数 must 称为 context ,而且 content 必须遵循。

  • 支持通过传递 name 注册时的参数。

  • 支持接受任意数量的位置或关键字参数。

  • 支持使用 as 变体。

内容逃避

simple_block_tag 表现类似于 simple_tag 关于自动逃生。有关逃生和安全的详细信息,请参阅 simple_tag .因为 content 姜戈已经提出了论点,它已经被逃脱了。

一个完整的示例

考虑一个自定义模板标签,它生成一个消息框,该消息框支持简单短语之外的多个消息级别和内容。这可以使用 simple_block_tag 具体如下:

testapp/templatetags/testapptags.py
from django import template
from django.utils.html import format_html


register = template.Library()


@register.simple_block_tag(takes_context=True)
def msgbox(context, content, level):
    format_kwargs = {
        "level": level.lower(),
        "level_title": level.capitalize(),
        "content": content,
        "open": " open" if level.lower() == "error" else "",
        "site": context.get("site", "My Site"),
    }
    result = """
    <div class="msgbox {level}">
      <details{open}>
        <summary>
          <strong>{level_title}</strong>: Please read for <i>{site}</i>
        </summary>
        <p>
          {content}
        </p>
      </details>
    </div>
    """
    return format_html(result, **format_kwargs)

当与最小视图和相应的模板相结合时,如此处所示:

testapp/views.py
from django.shortcuts import render


def simpleblocktag_view(request):
    return render(request, "test.html", context={"site": "Important Site"})
testapp/templates/test.html
{% extends "base.html" %}

{% load testapptags %}

{% block content %}

  {% msgbox level="error" %}
    Please fix all errors. Further documentation can be found at
    <a href="http://example.com">Docs</a>.
  {% endmsgbox %}

  {% msgbox level="info" %}
    More information at: <a href="http://othersite.com">Other Site</a>/
  {% endmsgbox %}

{% endblock %}

以下HTML作为渲染输出生成:

<div class="msgbox error">
  <details open>
    <summary>
      <strong>Error</strong>: Please read for <i>Important Site</i>
    </summary>
    <p>
      Please fix all errors. Further documentation can be found at
      <a href="http://example.com">Docs</a>.
    </p>
  </details>
</div>

<div class="msgbox info">
  <details>
    <summary>
      <strong>Info</strong>: Please read for <i>Important Site</i>
    </summary>
    <p>
      More information at: <a href="http://othersite.com">Other Site</a>
    </p>
  </details>
</div>

包含标签

django.template.Library.inclusion_tag()

另一种常见的模板标记类型是通过渲染显示某些数据的类型 另一个 模板。例如,Django的管理界面使用自定义模板标记显示“添加/更改”表单页面底部的按钮。这些按钮看起来总是一样的,但是链接目标会根据正在编辑的对象而变化——因此,对于使用由当前对象的详细信息填充的小模板来说,它们是一个完美的例子。(在管理员的情况下,这是 submit_row 标签)

这些类型的标签称为“包含标签”。

编写包含标签可能最好通过示例来演示。让我们编写一个标记,它为给定的 Poll 对象,例如在 tutorials . 我们将使用这样的标签:

{% show_results poll %}

…输出结果如下:

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

首先,定义接受参数并为结果生成数据字典的函数。这里重要的一点是我们只需要返回一本字典,而不是更复杂的东西。这将用作模板片段的模板上下文。例子::

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

接下来,创建用于呈现标签输出的模板。此模板是标签的固定功能:标签编写者指定它,而不是模板设计者。按照我们的例子,模板非常短:

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

现在,通过调用 inclusion_tag() A方法 Library 对象。按照我们的示例,如果上面的模板位于一个名为 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函数将有一个参数——从调用标记时起的模板上下文。

例如,假设您正在编写一个包含标记,该标记将始终用于包含 home_linkhome_title 指向主页面的变量。下面是python函数的样子:

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

注意函数的第一个参数 must 被称为 context .

register.inclusion_tag() 行,我们指定了 takes_context=True 以及模板的名称。这是模板 link.html 可能看起来像:

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

然后,只要您想使用这个自定义标记,就可以加载它的库,然后在不使用任何参数的情况下调用它,比如:

{% jump_link %}

注意当你使用 takes_context=True ,不需要向模板标记传递参数。它会自动访问上下文。

这个 takes_context 参数默认为 False . 当设置为 True ,标记将传递给上下文对象,如本例中所示。这是本案与前一案的唯一区别 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编译模板时,它将原始模板文本拆分为 nodes .每个节点都是 django.template.Node 并且具有 render() 法已编译的模板是一个列表 Node 对象当你叫 render() 在已编译的模板对象上,模板调用 render()Node 在其节点列表中,具有给定的上下文。 所有结果都连接在一起形成模板的输出。

因此,要定义自定义模板标记,可以指定如何将原始模板标记转换为 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() 不会像以前那样健壮 all 空格,包括引用字符串中的空格。最好总是使用 token.split_contents() .

  • 该职能部门负责提高 django.template.TemplateSyntaxError ,其中包含有用的消息,用于任何语法错误。

  • TemplateSyntaxError 异常使用 tag_name 变量不要在错误消息中硬编码标签的名称,因为这会将标签的名称与您的函数结合起来。 token.contents.split()[0]always 成为您标签的名称--即使标签没有参数。

  • 此函数返回 CurrentTimeNode 包含节点需要了解的有关此标签的所有信息。在这种情况下,它通过了论点-- "%Y-%m-%d %I:%M %p" .模板标签中的引言和尾引号将在中删除 format_string[1:-1]

  • 解析是非常低级的。Django开发人员已经尝试在这个解析系统的基础上,使用诸如ebnf语法之类的技术来编写小框架,但是这些实验使得模板引擎的速度太慢。这是低水平的,因为那是最快的。

正在写入渲染器

编写自定义标记的第二步是定义 Node 具有 render() 方法。

继续上面的例子,我们需要定义 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__() 得到 format_stringdo_current_time() . 始终将任何选项/参数/参数传递给 Node 通过其 __init__() .

  • 这个 render() 方法是实际发生工作的地方。

  • render() 通常会无声地失败,特别是在生产环境中。但在某些情况下,尤其是如果 context.template.engine.debugTrue ,此方法可能会引发异常以使调试更容易。例如,几个核心标签提升 django.template.TemplateSyntaxError 如果他们收到错误的参数数目或类型。

最终,这种编译和呈现的分离会产生一个高效的模板系统,因为一个模板可以呈现多个上下文,而无需多次解析。

自动转义注意事项

模板标记的输出是 not 自动运行自动转义过滤器(除了 simple_tag() 如上所述)。但是,在编写模板标记时,仍然需要记住一些事情。

如果 render() 模板标签的方法将结果存储在上下文变量中(而不是以字符串形式返回结果),请小心调用 mark_safe() 如果合适的话。当变量最终渲染时,它将受到当时有效的自动逸出设置的影响,因此应该安全地避免进一步逸出的内容需要标记为这样。

此外,如果模板标记为执行某些子渲染创建了新的上下文,请将自动转义属性设置为当前上下文的值。这个 __init__ 方法 Context 类接受一个名为 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字典,应用于存储 Node 调用之间的状态 render 方法。

让我们重构我们的 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 , the cyclevars 参数在 Node 是实例化的,因此我们不需要将其放入 render_context . 但是,说明特定于当前正在呈现的模板的信息,例如 CycleNode ,应存储在 render_context .

备注

注意我们如何使用 self 范围 CycleNode 中的特定信息 render_context . 可能有多个 CycleNodes 在给定的模板中,因此我们需要注意不要破坏另一个节点的状态信息。最简单的方法是始终使用 self 作为钥匙进入 render_context . 如果要跟踪几个状态变量,请 render_context[self] 字典。

注册标签

最后,将标签注册到您的模块的 Library 例如,正如中所解释的那样 writing custom template tags 以上示例::

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])

您还必须更改渲染器以检索 date_updated 性质 blog_entry 对象。这可以通过使用 Variable() 班在 django.template .

要使用 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() 获取块标签名称的多元组 to parse until .它返回 django.template.NodeList ,这是所有 Node 解析器遇到的对象 before 它遇到元组中命名的任何标记。

"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() . 但这次,我们通过了 nodelistNode ::

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()

这里唯一的新概念是 self.nodelist.render(context) 在里面 UpperNode.render() .

有关复杂呈现的更多示例,请参阅 {% for %} 在……里面 django/template/defaulttags.py{% if %} 在……里面 django/template/smartif.py