如何创建自定义模板标签和过滤器¶
Django 的模板语言自带了种类繁多的内置标签和过滤器,旨在满足应用程序的展示逻辑需求。然而,你可能会发现自己需要一些核心模板原语未涵盖的功能。你可以通过使用 Python 定义自定义标签和过滤器来扩展模板引擎,然后使用 {% load %} 标签让它们在模板中可用。
代码布局¶
指定自定义模板标签和过滤器的最常见位置是在 Django 应用内部。如果它们与现有的应用相关,将其捆绑在其中是有意义的;否则,可以将它们添加到一个新的应用中。当 Django 应用被添加到 INSTALLED_APPS 时,它在下述约定位置定义的任何标签都会自动变得可以在模板中加载。
该应用应该包含一个 templatetags 目录,与 models.py、views.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.py 和 django/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() 方法接收两个参数
过滤器的名称 —— 一个字符串。
编译函数 —— 一个 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_safe、needs_autoescape 和 expects_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. ...
模板过滤器代码属于以下两种情况之一
你的过滤器不会向结果中引入任何不安全的 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_safe为False,你可以在任何不需要它的过滤器中省略它。在决定你的过滤器是否真的让安全字符串保持安全时要小心。如果你正在 删除 字符,你可能会无意中在结果中留下不平衡的 HTML 标签或实体。例如,从输入中删除
>可能会将<a>变为<a,这需要在输出时进行转义以避免引起问题。同样,删除分号(;)会将&变为&,它不再是一个有效的实体,因此需要进一步转义。大多数情况不会那么棘手,但在审查代码时要留意此类问题。将过滤器标记为
is_safe会强制过滤器的返回值转换为字符串。如果你的过滤器应该返回布尔值或其他非字符串值,将其标记为is_safe可能会产生意外后果(例如将布尔值 False 转换为字符串 ‘False’)。或者,你的过滤器代码可以手动处理任何必要的转义。当你向结果中引入新的 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 的自定义过滤器,它结合了 urlize 和 linebreaksbr 过滤器,该过滤器看起来如下所示
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 ""
当设置此标志时,如果过滤器的第一个参数是感知时区的 datetime,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()。
如果你的模板标签需要访问当前上下文,你可以在注册标签时使用 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.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 块内有其他模板标签或变量,它们将在传递给标签函数之前被呈现。在上面的示例中,当调用 render_chart 时,request.user 将被解析。
块标签以 end{name} 结尾(例如 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以访问上下文。注意,在这种情况下,自定义函数的第一个参数必须命名为context,并且content必须紧随其后。支持通过在注册时传递
name参数来重命名标签。支持接受任意数量的位置参数或关键字参数。
支持使用
as变体将结果存储在模板变量中。
内容转义
simple_block_tag 在自动转义方面的表现与 simple_tag 类似。有关转义和安全性的详细信息,请参阅 simple_tag。因为 content 参数已经由 Django 呈现,所以它已经被转义了。
一个完整的示例¶
考虑一个生成消息框的自定义模板标签,该消息框支持多种消息级别和简单短语之外的内容。这可以使用 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 对象输出一个选项列表,就像在 教程 中创建的那样。我们将这样使用标签
{% 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>
现在,通过在 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 函数将有一个参数 —— 即调用该标签时的模板上下文。
例如,假设你正在编写一个始终在包含指向主页的 home_link 和 home_title 变量的上下文中使用包含标签。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 时,无需将参数传递给模板标签。它会自动获得对上下文的访问权限。
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() 时,模板会使用给定的上下文对节点列表中的每个 Node 调用 render()。结果被连接在一起,形成模板的输出。
因此,要定义自定义模板标签,你需要指定原始模板标签如何转换为 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.debug为True,此方法可能会引发异常以使调试更容易。例如,如果核心标签接收到错误的参数数量或类型,它们中的几个会引发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 执行其第一次循环迭代,
CycleNode.render()返回 ‘row1’线程 2 执行其第一次循环迭代,
CycleNode.render()返回 ‘row2’线程 1 执行其第二次循环迭代,
CycleNode.render()返回 ‘row1’线程 2 执行其第二次循环迭代,
CycleNode.render()返回 ‘row2’
CycleNode 正在迭代,但它是在全局范围内迭代的。对于线程 1 和线程 2 而言,它总是返回相同的值。这不是我们想要的!
为了解决这个问题,Django 提供了一个与当前正在呈现的模板的 context 相关联的 render_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() 方法接受两个参数
模板标签的名称 —— 一个字符串。如果省略,将使用编译函数的名称。
编译函数 —— 一个 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() 将返回三个值
标签名称
format_time。字符串
'blog_entry.date_updated'(不带周围的引号)。格式字符串
'"%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 中的源代码。