本文从技术的角度解释Django 模板系统 —— 它如何工作以及如何继承它。如果你正在查找语言语法方面的参考,参见Django 模板语言。
假设你已经理解了模板、上下文、变量、标签和渲染。如果你不熟悉这些概念,从阅读 Django 模板语言起步吧。
在Python中使用模板系统有三个步骤:
对于这些步骤,Django 的项目一般使用高级的、与后端无关的API ,而不用模板系统的底层API:
实例化Engine 时,所有的参数必须通过关键字参数传递:
dirs 是一个列表,包含引擎查找模板源文件的目录。它用于配置filesystem.Loader。
默认为一个空的列表。
app_dirs 只影响loaders 的默认值。参见下文。
默认为False。
allowed_include_roots 是一个字符串列表,它们表示{% ssi %} 模板标签的前缀。这是一个安全措施,让模板的作者不能访问他们不应该访问的文件。
例如,如果'allowed_include_roots' 为['/home/html', '/var/www'],那么{% ssi /home/html/foo.txt %} 可以工作,而{% ssi /etc/passwd %} 不能工作。
默认为一个空的列表。
自1.8版起已弃用:废弃allowed_include_roots。
context_processors 是一个Python 可调用对象的路径列表,它们用于模板渲染时填充其上下文。这些可调用对象接收一个HTTP 请求对象作为它们的参数,并返回一个 字典 用于合并到上下文中。
它默认为一个空的列表。
更多信息,参见RequestContext。
debug 是一个布尔值,表示打开/关闭模板的调试模式。如果为True,模板引擎将保存额外的调试信息,这些信息可以用来显示模板渲染过程中引发的异常的详细报告。
它默认为False。
loaders 是模板加载类的一个列表,这些类由字符串表示。每个Loader 类知道如何从一个特定的源导入模板。还可以使用元组代替字符串表示这些类。元组的第一个元素应该是Loader 类的名称,接下来的元素将在Loader 初始化时用于初始化。
它默认为包含下面内容的列表:
细节参见Loader types。
string_if_invalid 表示模板系统遇到不合法(例如,拼写错误)的变量时应该使用的字符串。
默认为空字符串。
细节参见如何处理不合法的变量。
file_charset 是读取磁盘上的模板文件使用的字符集。
默认为'utf-8'。
当Django 项目配置仅配置一个DjangoTemplates 引擎时,这个方法返回底层的引擎。在其它情况下,它将引发ImproperlyConfigured。
它是保留依赖于全局可用的隐式配置引擎的API所必需的。强烈不鼓励任何其他用途。
类似get_template(),不同的是它接收一个名称列表并返回找到的第一个模板。
建议调用Engine 的工厂方法创建Template:get_template()、select_template() 和 from_string()。
在TEMPLATES 只定义一个DjangoTemplates 引擎的Django 项目中,可以直接实例化Template。
这个类位于django.template.Template。其构造函数接收一个参数 —— 原始的模板代码:
from django.template import Template
template = Template("My name is {{ my_name }}.")
幕后
系统只会解析一次原始的模板代码 —— 当创建Template 对象的时候。在此之后,处于性能考虑,会在内部将它存储为一个树形结构。
解析器本身非常快。大部分解析的动作只需要调用一个简短的正则表达式。
一旦编译好Template 对象,你就可以用上下文渲染它了。你可以使用不同的上下文多次重新渲染相同的模板。
这个类位于django.template.Context。其构造函数接收两个可选的参数:
一个字典,映射变量名到变量的值。
当前应用的名称。该应用的名称用于帮助解析带命名空间的URLs。如果没有使用带命名空间的URL,可以忽略这个参数。
自1.8版起已弃用:废弃current_app 参数。如果需要它,则必须使用RequestContext 代替Context。
细节参见下文的使用上下文对象。
使用Context 调用Template 对象的render() 方法来“填充”模板:
>>> from django.template import Context, Template
>>> template = Template("My name is {{ my_name }}.")
>>> context = Context({"my_name": "Adrian"})
>>> template.render(context)
"My name is Adrian."
>>> context = Context({"my_name": "Dolores"})
>>> template.render(context)
"My name is Dolores."
变量名必须由字母、数字、下划线(不能以下划线开头)和点组成。
点在模板渲染时有特殊的含义。变量名中点表示查找。具体一点,当模板系统遇到变量名中的一个点时,它会按下面的顺序进行查找:
注意,像{{ foo.bar }} 这种模版表达式中的“bar”,如果在模版上下文中存在,将解释为一个字符串字面量而不是使用变量“bar”的值。
模板系统使用找到的第一个可用的类型。这是一个短路逻辑。下面是一些示例:
>>> from django.template import Context, Template
>>> t = Template("My name is {{ person.first_name }}.")
>>> d = {"person": {"first_name": "Joe", "last_name": "Johnson"}}
>>> t.render(Context(d))
"My name is Joe."
>>> class PersonClass: pass
>>> p = PersonClass()
>>> p.first_name = "Ron"
>>> p.last_name = "Nasty"
>>> t.render(Context({"person": p}))
"My name is Ron."
>>> t = Template("The first stooge in the list is {{ stooges.0 }}.")
>>> c = Context({"stooges": ["Larry", "Curly", "Moe"]})
>>> t.render(c)
"The first stooge in the list is Larry."
如果变量的任何部分是可调用的,模板系统将尝试调用它。例如:
>>> class PersonClass2:
... def name(self):
... return "Samantha"
>>> t = Template("My name is {{ person.name }}.")
>>> t.render(Context({"person": PersonClass2}))
"My name is Samantha."
可调用的变量比只需直接查找的变量稍微复杂一些。需要记住下面几点:
如果变量在调用时引发一个异常,该异常将会传播,除非该异常的silent_variable_failure 属性的值为True。如果异常确实 具有一个silent_variable_failure 属性且值为True,该变量将渲染成引擎的string_if_invalid 配置的值(默认为一个空字符串)。例如:
>>> t = Template("My name is {{ person.first_name }}.")
>>> class PersonClass3:
... def first_name(self):
... raise AssertionError("foo")
>>> p = PersonClass3()
>>> t.render(Context({"person": p}))
Traceback (most recent call last):
...
AssertionError: foo
>>> class SilentAssertionError(Exception):
... silent_variable_failure = True
>>> class PersonClass4:
... def first_name(self):
... raise SilentAssertionError
>>> p = PersonClass4()
>>> t.render(Context({"person": p}))
"My name is ."
注意,django.core.exceptions.ObjectDoesNotExist 的silent_variable_failure = True,它是Django 数据库API 所有DoesNotExist 异常的基类。所以,如果你在Django 模板中使用Django 模型对象,任何 DoesNotExist 异常都将默默地失败。
只有在变量不需要参数时才可调用。否则,系统将返回引擎的string_if_invalid 选项。
很显然,调用某些变量会带来副作用,允许模板系统访问它们将是愚蠢的还会带来安全漏洞。
每个Django 模型对象的delete() 方法就是一个很好的例子。模板系统不应该允许下面的行为:
I will now delete this valuable data. {{ data.delete }}
设置可调用变量的alters_data 属性可以避免这点。如果变量设置alters_data=True ,模板系统将不会调用它,而会无条件使用string_if_invalid 替换这个变量。Django 模型对象自动生成的delete() 和save() 方法自动 设置alters_data=True。 例如:
def sensitive_function(self):
self.database_record.delete()
sensitive_function.alters_data = True
有时候,处于某些原因你可能想关闭这个功能,并告诉模板系统无论什么情况下都不要调用变量。设置可调用对象的do_not_call_in_templates 属性的值为True 可以实现这点。模板系统的行为将类似这个变量是不可调用的(例如,你可以访问可调用对象的属性)。
一般情况下,如果变量不存在,模板系统会插入引擎string_if_invalid 配置的值,其默认设置为''(空字符串)。
过滤器只有在string_if_invalid 设置为''(空字符串)时才会应用到不合法的变量上。如果string_if_invalid 设置为任何其它的值,将会忽略变量的过滤器。
这个行为对于if、for 和 regroup 模板标签有些不同。如果这些模板便签遇到不合法的变量,会将该变量解释为None。在这些模板标签中的过滤器对不合法的变量也会始终应用。
如果string_if_invalid 包含'%s',这个格式标记将会被非法变量替换。
只用于调试目的!
虽然string_if_invalid 是一个很有用的调试工具,但是,将它作为“默认的开发工具”是个很糟糕的主意。
许多模板包括Admin 站点,在遇到不存在的变量时,依赖模板系统的沉默行为。如果你赋值非'' 的值给string_if_invalid,使用这些模板和站点可能遇到渲染上的问题。
一般情况下,只有在调试一个特定的模板问题时才启用string_if_invalid,一旦调试完成就要清除。
每个上下文都包含True、False 和 None。和你期望的一样,这些变量将解析为对应的Python 对象。
Django 的模板语言没有办法转义它自己的语法用到的字符。例如,如果你需要输出字符序列{% 和%},你需要用到templatetag 标签。
如果你想在模板过滤器或标签中包含这些序列,会存在类似的问题。例如,当解析block 标签时,Django 的模板解析器查找%} 之后出现的第一个{%。这将导致不能使用"%}" 这个字符串字面量。例如,下面的表达式将引发一个TemplateSyntaxError:
{% include "template.html" tvar="Some string literal with %} in it." %}
{% with tvar="Some string literal with %} in it." %}{% endwith %}
在过滤器参数中使用反向的序列会触发同样的问题:
{{ some.variable|default:"}}" }}
如果你需要使用这些字符串序列,可以将它们保存在模板变量中,或者自定义模板标签或过滤器来绕过这个限制。
大部分时候,你将通过传递一个完全填充的字典给Context() 来实例化一个Context 对象。你也可以使用标准的字典语法在Context 对象实例化之后,向它添加和删除元素:
>>> from django.template import Context
>>> c = Context({"foo": "bar"})
>>> c['foo']
'bar'
>>> del c['foo']
>>> c['foo']
Traceback (most recent call last):
...
KeyError: 'foo'
>>> c['newvariable'] = 'hello'
>>> c['newvariable']
'hello'
如果key 在Context 中,则返回key 的值,否则返回otherwise。
Context 对象是一个栈。也就是说,你可以push() 和pop() 它。如果你pop() 得太多,它将引发django.template.ContextPopException:
>>> c = Context()
>>> c['foo'] = 'first level'
>>> c.push()
{}
>>> c['foo'] = 'second level'
>>> c['foo']
'second level'
>>> c.pop()
{'foo': 'second level'}
>>> c['foo']
'first level'
>>> c['foo'] = 'overwritten'
>>> c['foo']
'overwritten'
>>> c.pop()
Traceback (most recent call last):
...
ContextPopException
你还可以使用push() 作为上下文管理器以确保调用对应的pop()。
>>> c = Context()
>>> c['foo'] = 'first level'
>>> with c.push():
... c['foo'] = 'second level'
... c['foo']
'second level'
>>> c['foo']
'first level'
所有传递给push() 的参数将传递给dict 构造函数用于构造新的上下文层级。
>>> c = Context()
>>> c['foo'] = 'first level'
>>> with c.push(foo='second level'):
... c['foo']
'second level'
>>> c['foo']
'first level'
除了push() 和pop() 之外,Context 对象还定义一个update() 方法。它的工作方式类似push(),不同的是它接收一个字典作为参数并将该字典压入栈。
>>> c = Context()
>>> c['foo'] = 'first level'
>>> c.update({'foo': 'updated'})
{'foo': 'updated'}
>>> c['foo']
'updated'
>>> c.pop()
{'foo': 'updated'}
>>> c['foo']
'first level'
将Context 用作栈在一些自定义的标签中 非常方便。
利用flatten() 方法,你可以获得字典形式的全部Context 栈,包括内建的变量。
>>> c = Context()
>>> c['foo'] = 'first level'
>>> c.update({'bar': 'second level'})
{'bar': 'second level'}
>>> c.flatten()
{'True': True, 'None': None, 'foo': 'first level', 'False': False, 'bar': 'second level'}
flatten() 方法在内部还被用来比较Context 对象。
>>> c1 = Context()
>>> c1['foo'] = 'first level'
>>> c1['bar'] = 'second level'
>>> c2 = Context()
>>> c2.update({'bar': 'second level', 'foo': 'first level'})
{'foo': 'first level', 'bar': 'second level'}
>>> c1 == c2
True
在单元测试中,flatten() 的结果可以用于比较Context 和dict:
class ContextTest(unittest.TestCase):
def test_against_dictionary(self):
c1 = Context()
c1['update'] = 'value'
self.assertEqual(c1.flatten(), {
'True': True,
'None': None,
'False': False,
'update': 'value',
})
Django 带有一个特殊的Context 类django.template.RequestContext,它与通常的django.template.Context 行为有少许不同。第一个不同点是,它接收一个HttpRequest 作为第一个参数。例如:
c = RequestContext(request, {
'foo': 'bar',
})
第二个不同点是,它根据引擎的 context_processors 配置选项自动向上下文中填充一些变量。
context_processors 选项是一个可调用对象 —— 叫做上下文处理器 —— 的列表,它们接收一个请求对象作为它们的参数并返回需要向上下文中添加的字典。在生成的默认设置文件中,默认的模板引擎包含下面几个上下文处理器:
[
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
]
Django 1.8 将模板内建的上下文处理器从django.core.context_processors 移动到django.template.context_processors中。
除此之外,RequestContext 始终启用'django.template.context_processors.csrf'。这是一个安全相关的上下文处理器,Admin 和其它Contrib 应用需要它,而且为了防止意外的错误配置,它被有意硬编码在其中且在context_processors 选项中不可以关闭。
每个处理器按顺序启用。这意味着,如果一个处理器向上下文添加一个变量,而第二个处理器添加一个相同名称的变量,第二个将覆盖第一个。默认的处理器会在下面解释。
上下文处理器应用的时机
上下文处理器应用在上下文数据的顶端。也就是说,上下文处理器可能覆盖你提供给Context 或RequestContext 的变量,所以要注意避免与上下文处理器提供的变量名重复。
如果想要上下文数据的优先级高于上下文处理器,使用下面的模式:
from django.template import RequestContext
request_context = RequestContext(request)
request_context.push({"my_name": "Adrian"})
Django 通过这种方式允许上下文数据在render() 和 TemplateResponse 等API 中覆盖上下文处理器。
你还可以赋予RequestContext 一个额外的处理器列表,使用第三个可选的位置参数processors。在下面的示例中,RequestContext 实例获得一个ip_address 变量:
from django.http import HttpResponse
from django.template import RequestContext
def ip_address_processor(request):
return {'ip_address': request.META['REMOTE_ADDR']}
def some_view(request):
# ...
c = RequestContext(request, {
'foo': 'bar',
}, [ip_address_processor])
return HttpResponse(t.render(c))
下面是每个内置的上下文处理器所做的事情:
如果启用这个处理器,每个RequestContext 将包含以下变量:
如果开启这个处理器,每一个RequestContext将会包含两个变量—但是只有当你的 DEBUG配置设置为True时有效。请求的IP地址 (request.META ['REMOTE_ADDR'])位于INTERNAL_IPS设置中:
如果启用这个处理器,每个RequestContext 将包含两个变量:
更多信息,参见国际化和本地化。
如果启用这个处理器,每个RequestContext 将包含一个MEDIA_URL 变量,表示MEDIA_URL 设置的值。
如果启用这个处理器,每个RequestContext 将包含一个STATIC_URL 变量,表示STATIC_URL 设置的值。
上下文处理器添加一个token,这个token是 csrf_token 模版标签需要的,用来针对Cross Site Request Forgeries.
如果启用这个处理器,每个RequestContext 将包含一个request 变量,表示当前的HttpRequest。
一个上下文处理器有一个非常简单的接口:它是一个参数的Python函数,这个参数是一个HttpRequest对象,并且返回一个字典,这个字典会被添加到模版上下文中。每个上下文处理器必须返回一个字典。
自定义上下文处理器可以在您的代码库中的任何地方。所有Django都关心您的自定义上下文处理器是由TEMPLATES设置中的'context_processors'选项指向的,或者是context_processors Engine(如果您直接使用它)。
通常情况下,你会将模板存储在文件系统上的文件中而不是自己使用底层的Template API。保存模板的目录叫做模板目录。
Django 在许多地方查找模板目录,这取决于你的模板加载设置(参见下文中的“加载器类型”),但是指定模板目录最基本的方法是使用DIRS 选项。
这个值以前通过TEMPLATE_DIRS 设置定义。
设置文件中TEMPLATES 设置的DIRS 选项或者Engine 的dirs 参数用于告诉Django 你的模板目录。它应该设置为一个字符串列表,包含模板目录的完整路径:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
'/home/html/templates/lawrence.com',
'/home/html/templates/default',
],
},
]
模板可以位于任何位置,只要Web 服务器可以读取这些目录和模板。它们可以具有任何扩展名例如.html 或.txt,或者完全没有扩展名。
注意,这些路径应该使用Unix 风格的前向斜杠,即使在Windows 上。
默认情况下,Django 使用基于文件系统的模板加载器,但是Django 自带几个其它的模板加载器,它们知道如何从其它源加载模板。
默认情况下,某些其它的加载器是被禁用的,但是你可以向TEMPLATES 设置中的DjangoTemplates 添加一个'loaders' 选项或者传递一个loaders 参数给Engine 来激活它们。loaders 应该为一个字符串列表或元组,每个字符串表示一个模板加载器类。下面是Django 自带的模板加载器:
django.template.loaders.filesystem.Loader
根据DIRS,从文件系统加载模板。
该加载器默认是启用的。当然,只有你将DIRS 设置为一个非空的列表,它才能找到模板:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
}]
django.template.loaders.app_directories.装载机
从文件系统加载Django 应用中的模板。对于INSTALLED_APPS 中的每个应用,该加载器会查找其下面的一个templates 子目录。如果该目录存在,Django 将在那里查找模板。
这意味着你可以将模板保存在每个单独的应用中。这还使得发布带有默认模板的Django 应用非常方便。
例如,对于这个设置:
INSTALLED_APPS = ('myproject.polls', 'myproject.music')
...get_template('foo.html') 将按顺序在下面的目录中查找foo.html:
...并将使用第一个找到的模板。
INSTALLED_APPS 的顺序非常重要!例如,如果你想自定义Django Admin,你可能选择使用myproject.polls 中自己的admin/base_site.html覆盖django.contrib.admin 中标准的admin/base_site.html 。那么你必须确保在INSTALLED_APPS 中myproject.polls 位于django.contrib.admin之前,否则仍将加载 django.contrib.admin 中的模板并忽略你自己的模板。
注意,加载器在第一次运行时会做一些优化:它缓存一个含具有 templates 子目录的INSTALLED_APPS 包的列表。
你可以简单地通过设置APP_DIRS 为True 来启用这个加载器:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
}]
django.template.loaders.eggs.Loader
与上面的app_directories 类似,只是它从Python eggs 中而不是从文件系统中加载模板。
这个加载器在默认情况下是禁用的。
django.template.loaders.cached.Loader
默认情况下,每当模版需要被渲染,模版系统将会读取和编译你的模版。但是,Django模版系统是非常高速的, the overhead from reading and compiling templates can add up.
基于缓存的模版加载器是一个基于类的加载器,that you configure with a list of other loaders that it should wrap.包装加载器用于在未知模板首次遇到时定位它们。接下来,基于缓存加载器将编译过的Template存储在内存中。为加载相同模板的后续请求返回缓存的Template实例。
例如,要使用filesystem和app_directories模板加载器启用模板缓存,您可以使用以下设置:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
]),
],
},
}]
注意
所有内置的Django模板标签都可以安全地用于缓存加载器,但如果您使用来自第三方软件包或自己编写的自定义模板标签,则应确保Node有关详细信息,请参阅template tag thread safety considerations。
这个加载器在默认情况下是禁用的。
django.template.loaders.locmem.Loader
从一个Python 目录中加载模板。它主要用于测试。
这个加载器接收一个模板字典作为它的第一个参数:
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'OPTIONS': {
'loaders': [
('django.template.loaders.locmem.Loader', {
'index.html': 'content here',
}),
],
},
}]
这个加载器在默认情况下是禁用的。
Django 按照'loaders' 中选项的顺序使用模板加载器。它逐个使用每个加载器直到某个加载器找到一个匹配的模板。
自定义加载器应该继承django.template.loaders.base.Loader 并覆盖load_template_source() 方法,这个方法接收一个template_name 参数、从磁盘(或其它地方)加载模板、然后返回一个元组:(template_string, template_origin)。
django.template.loaders.base.Loader 以前定义在 django.template.loader.BaseLoader 中。
Loader类的load_template() 方法通过调用load_template_source() 获取模板字符串、从模板源中实例化一个Template、然后返回一个元组:(template, template_origin)。
2015年5月13日