-
python全栈开发中Django的深入模板引擎
第十章: 深入模板引擎
虽然和Django的模板语言的大多数交互都是模板作者的工作,但你可能想定制和扩展模板引擎,让它做一些它不能做的事情,或者是以其他方式让你的工作更轻松。
python入门教程本章深入钻研Django的模板系统。如果你想扩展模板系统或者只是对它的工作原理感觉到好奇,本章涉及了你需要了解的东西。
如果你想把Django的模版系统作为另外一个应用程序的一部分(比如,仅使用django的模板系统而不使用Django框架的其他部分),那你一定要读一下“配置独立模式下的模版系统”这一节。
模板语言回顾
python入门教程首先,让我们快速回顾一下第四章介绍的若干专业术语
模板 是一个纯文本文件,或是一个用Django模板语言标记过的普通的Python字符串,一个模板可以包含区块标签和变量。
区块标签 是在一个模板里面起作用的的标记,这个定义故意说的很含糊,比如,一个 区块标签可以生成内容,可以作为一个控制结构( if 语句或 for 循环), 可以获取数据库内容,或者访问其他的模板标签。
区块标签被 {% 和 %} 环绕:
{% if is_logged_in %}
Thanks for logging in!
{% else %}
Please log in.
{% endif %}
变量 是一个在模板里用来输出值的标记。
变量标签被 {{ 和 }} 环绕:
My first name is {{ first_name }}. My last name is {{ last_name }}.
context 是一个传递给模板的名称到值的映射(类似Python字典)。
模板 渲染 就是是通过从context获取值来替换模板中变量并执行所有的区块标签。
关于这些基本概念更详细的内容,请参考第四章。
本章的其余部分讨论了扩展模板引擎的方法。首先,我们快速的看一下第四章遗留的内容。
RequestContext和Context处理器
你需要一段context来解析模板。一般情况下,这是一个 django.template.Context 的实例,不过在Django中还可以用一个特殊的子类, django.template.RequestContext ,这个运用起来稍微有些不同。 RequestContext 默认地在模板context中加入了一些变量,如 HttpRequest 对象或当前登录用户的相关信息。
当你不想在一系例模板中都明确指定一些相同的变量时,你应该使用 RequestContext 。例如,看下面的四个视图:
from django.template import loader, Context
def view_1(request):
# ...
t = loader.get_template('template1.html')
c = Context({
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR'],
'message': 'I am view 1.'
})
return t.render(c)
def view_2(request):
# ...
t = loader.get_template('template2.html')
c = Context({
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR'],
'message': 'I am the second view.'
})
return t.render(c)
def view_3(request):
# ...
t = loader.get_template('template3.html')
c = Context({
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR'],
'message': 'I am the third view.'
})
return t.render(c)
def view_4(request):
# ...
t = loader.get_template('template4.html')
c = Context({
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR'],
'message': 'I am the fourth view.'
})
return t.render(c)
(注意,在这些例子中,我们故意 不 使用 rendertoresponse() 这个快捷方法,而选择手动载入模板,手动构造context对象然后渲染模板。是为了能够清晰的说明所有步骤。)
每个视图都给模板传入了三个相同的变量: app 、 user 和 ip_address 。如果我们能把这些冗余去掉会不会看起来更好?
创建 RequestContext 和 context处理器 就是为了解决这个问题。Context处理器允许你设置一些变量,它们会在每个context中自动被设置好,而不必每次调用 rendertoresponse() 时都指定。要点就是,当你渲染模板时,你要用 RequestContext 而不是 Context 。
最直接的做法是用context处理器来创建一些处理器并传递给 RequestContext 。上面的例子可以用context processors改写如下:
from django.template import loader, RequestContext
def custom_proc(request):
"A context processor that provides 'app', 'user' and 'ip_address'."
return {
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR']
}
def view_1(request):
# ...
t = loader.get_template('template1.html')
c = RequestContext(request, {'message': 'I am view 1.'},
processors=[custom_proc])
return t.render(c)
def view_2(request):
# ...
t = loader.get_template('template2.html')
c = RequestContext(request, {'message': 'I am the second view.'},
processors=[custom_proc])
return t.render(c)
def view_3(request):
# ...
t = loader.get_template('template3.html')
c = RequestContext(request, {'message': 'I am the third view.'},
processors=[custom_proc])
return t.render(c)
def view_4(request):
# ...
t = loader.get_template('template4.html')
c = RequestContext(request, {'message': 'I am the fourth view.'},
processors=[custom_proc])
return t.render(c)
我们来通读一下代码:
§ 首先,我们定义一个函数 custom_proc 。这是一个context处理器,它接收一个 HttpRequest 对象,然后返回一个字典,这个字典中包含了可以在模板context中使用的变量。它就做了这么多。
§ 我们在这四个视图函数中用 RequestContext 代替了 Context 。在context对象的构建上有两个不同点。一,RequestContext 的第一个参数需要传递一个 HttpRequest 对象,就是传递给视图函数的第一个参数( request )。二,RequestContext 有一个可选的参数 processors ,这是一个包含context处理器函数的list或者tuple。在这里,我们传递了我们之前定义的函数 curstom_proc 。
§ 每个视图的context结构里不再包含 app 、 user 、 ipaddress 等变量,因为这些由 customproc 函数提供了。
§ 每个视图 仍然 具有很大的灵活性,可以引入我们需要的任何模板变量。在这个例子中, message 模板变量在每个视图中都不一样。
在第四章,我们介绍了 rendertoresponse() 这个快捷方式,它可以省掉调用 loader.gettemplate() ,然后创建一个 Context对象,最后再调用模板对象的 render() 方法。为了讲解context处理器底层是如何工作的,在上面的例子中我们没有使用rendertoresponse() 。但是建议选择 rendertoresponse() 作为context的处理器。像这样,使用 contextinstance 参数:
from django.shortcuts import render_to_response
from django.template import RequestContext
def custom_proc(request):
"A context processor that provides 'app', 'user' and 'ip_address'."
return {
'app': 'My app',
'user': request.user,
'ip_address': request.META['REMOTE_ADDR']
}
def view_1(request):
# ...
return render_to_response('template1.html',
{'message': 'I am view 1.'},
context_instance=RequestContext(request, processors=[custom_proc]))
def view_2(request):
# ...
return render_to_response('template2.html',
{'message': 'I am the second view.'},
context_instance=RequestContext(request, processors=[custom_proc]))
def view_3(request):
# ...
return render_to_response('template3.html',
{'message': 'I am the third view.'},
context_instance=RequestContext(request, processors=[custom_proc]))
def view_4(request):
# ...
return render_to_response('template4.html',
{'message': 'I am the fourth view.'},
context_instance=RequestContext(request, processors=[custom_proc]))
在这,我们将每个视图的模板渲染代码写成了一个单行。
虽然这是一种改进,但是,请考虑一下这段代码的简洁性,我们现在不得不承认的是在 另外 一方面有些过分了。我们以代码冗余(在 processors 调用中)的代价消除了数据上的冗余(我们的模板变量)。由于你不得不一直键入 processors ,所以使用context处理器并没有减少太多的打字次数。
Django因此提供对 全局 context处理器的支持。 TEMPLATECONTEXTPROCESSORS 指定了 总是 使用哪些 context processors 。这样就省去了每次使用 python入门教程RequestContext 都指定 processors 的麻烦^_^。
默认情况下, TEMPLATECONTEXTPROCESSORS 设置如下:
TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
)
这个设置是一个可调用函数的Tuple,其中的每个函数使用了和上文中我们的 customproc 相同的接口:接收一个request对象作为参数,返回一个包含了将被合并到context中的项的字典。请注意 TEMPLATECONTEXT_PROCESSORS 中的值是以 strings 的形式给出的,这意味着这些处理器必须在你的python路径中的某处(这样你才能在设置中引用它们)
每个处理器将会按照顺序应用。也就是说如果你在第一个处理器里面向context添加了一个变量,而第二个处理器添加了同样名字的变量,那么第二个将会覆盖第一个。
Django提供了几个简单的context处理器,有些在默认情况下被启用的。
django.core.context_processors.auth
如果 TEMPLATECONTEXTPROCESSORS 包含了这个处理器,那么每个 RequestContext 将包含这些变量:
§ user :一个 django.contrib.auth.models.User 实例,描述了当前登录用户(或者一个 AnonymousUser 实例,如果客户端没有登录)。
§ messages :一个当前登录用户的消息列表(字符串)。在后台,对每一个请求这个变量都调用request.user.getanddelete_messages() 方法。这个方法收集用户的消息然后把它们从数据库中删除。
§ perms : django.core.context_processors.PermWrapper 的一个实例,包含了当前登录用户有哪些权限。
关于users、permissions和messages的更多内容请参考第12章。
django.core.context_processors.debug
这个处理器把调试信息发送到模板层。如果 TEMPLATECONTEXTPROCESSORS 包含了这个处理器, RequestContext 将包含这些变量:
§ debug :你设置的 DEBUG 的值( True 或 False )。你可以在模板里面用这个变量测试是否处在debug模式下。
§ sql_queries :包含类似于 {'sql': ..., 'time': ...} 的字典的一个列表, 记录了这个请求期间的每个SQL查询以及查询所耗费的时间。这个列表是按照请求顺序进行排列的。
由于调试信息比较敏感,所以这个context处理器只有当同时满足下面两个条件的时候才有效:
§ DEBUG 参数设置为 True 。
§ 请求的ip应该包含在 INTERNAL_IPS 的设置里面。
django.core.context_processors.i18n
如果这个处理器启用,每个 RequestContext 将包含下面的变量:
§ LANGUAGES : LANGUAGES 选项的值。
§ LANGUAGECODE :如果 request.LANGUAGECODE 存在,就等于它;否则,等同于 LANGUAGE_CODE 设置。
附录E提供了有关这两个设置的更多的信息。
django.core.context_processors.request
如果启用这个处理器,每个 RequestContext 将包含变量 request , 也就是当前的 HttpRequest 对象。注意这个处理器默认是不启用的,你需要激活它。
写Context处理器的一些建议
编写处理器的一些建议:
§ 使每个context处理器完成尽可能小的功能。 使用多个处理器是很容易的,所以你可以根据逻辑块来分解功能以便将来重用。
§ 要注意 TEMPLATECONTEXTPROCESSORS 里的context processor 将会在 每个 模板中有效,所以要变量的命名不要和模板的变量冲突。 变量名是大小写敏感的,所以processor的变量全用大写是个不错的主意。
§ 只要它们存放在你的Python的搜索路径中,它们放在哪个物理路径并不重要,这样你可以在TEMPLATECONTEXTPROCESSORS 设置里指向它们。 也就是说,你要把它们放在app或者project目录里名为context_processors.py 的文件。
模板加载的内幕
一般说来,你会把模板以文件的方式存储在文件系统中,但是你也可以使用自定义的 template loaders 从其他来源加载模板。
Django有两种方法加载模板
§ django.template.loader.gettemplate(templatename) : get_template 根据给定的模板名称返回一个已编译的模板(一个 Template 对象)。如果模板不存在,就触发 TemplateDoesNotExist 的异常。
§ django.template.loader.selecttemplate(templatename_list) : selecttemplate 很像 gettemplate ,不过它是以模板名称的列表作为参数的,并且它返回第一个存在的模板。如果模板都不存在,将会触发 TemplateDoesNotExist 异常。
正如在第四章中所提到的,默认情况下这些函数使用 TEMPLATE_DIRS 的设置来载入模板。但是,在内部这些函数可以指定一个模板加载器来完成这些繁重的任务。
一些加载器默认被禁用,但是你可以通过编辑 TEMPLATELOADERS 设置来激活它们。 TEMPLATELOADERS 应当是一个字符串的元组,其中每个字符串都表示一个模板加载器。这些模板加载器随Django一起发布。
django.template.loaders.filesystem.loadtemplatesource : 这个加载器根据TEMPLATE_DIRS 的设置从文件系统加载模板。在默认情况下这个加载器被启用.
django.template.loaders.appdirectories.loadtemplatesource : 这个加 载器从文件系统上的Django应用中加载模板。对 INSTALLEDAPPS 中的每个应用,这个加 载器会查找一个templates 子目录。如果这个目录存在,Django就在那里寻找模板。
这意味着你可以把模板和你的应用一起保存,从而使得Django应用更容易和默认模板一起发布。例如,如果 INSTALLEDAPPS 包含 ('myproject.polls','myproject.music') ,那么gettemplate('foo.html') 会按这个顺序查找模板:
§ /path/to/myproject/polls/templates/foo.html
§ /path/to/myproject/music/templates/foo.html
请注意加载器在首次被导入的时候会执行一个优化:它会缓存一个列表,这个列表包含了INSTALLED_APPS 中带有 templates 子目录的包。
这个加载器默认启用。
django.template.loaders.eggs.loadtemplatesource : 这个加载器类似 app_directories,只不过它从Python eggs而不是文件系统中加载模板。这个加载器默认被禁用;如果你使用eggs来发布你的应用,那么你就需要启用它。
Django按照 TEMPLATE_LOADERS 设置中的顺序使用模板加载器。它逐个使用每个加载器直至找到一个匹配的模板。
扩展模板系统
既然你已经对模板系统的内幕了解多了一些,让我们来看看如何使用自定义的代码来拓展这个系统吧。
绝大部分的模板定制是以自定义标签和/或过滤器的方式来完成的。尽管Django模板语言自带了许多内建标签和过滤器,但是你可能还是需要组建你自己的标签和过滤器库来满足你的需要。幸运的是,定义你自己的功能非常容易。
创建一个模板库
不管是写自定义标签还是过滤器,第一件要做的事是给 template library 创建使Django能够勾入的机制。
创建一个模板库分两步走:
第一,决定哪个Django应用应当拥有这个模板库。如果你通过 manage.py startapp 创建了一个应用,你可以把它放在那里,或者你可以为模板库单独创建一个应用。
无论你采用何种方式,请确保把你的应用添加到 INSTALLED_APPS 中。我们稍后会解释这一点。
第二,在适当的Django应用包里创建一个 templatetags 目录。这个目录应当和 models.py 、views.py 等处于同一层次。例如:
books/
__init__.py
models.py
templatetags/
views.py
在 templatetags 中创建两个空文件:一个 init.py (告诉Python这是 一个包含了Python代码的包)和一个用来存放你自定义的标签/过滤器定义的文件。第二个文件 的名字稍后将用来加载标签。例如,如果你的自定义标签/过滤器在一个叫作 poll_extras.py 的文件中,你需要在模板中写入如下内容:
{% load poll_extras %}
{% load %} 标签检查 INSTALLED_APPS 中的设置,仅允许加载已安装的Django应用程序中的模板库。这是一个安全特性。它可以让你在一台电脑上部署很多的模板库的代码,而又不用把它们暴露给每一个Django安装。
如果你写了一个不和任何模型/视图关联的模板库,那么得到一个仅包含 templatetags 包的Django应用程序包是完全正常的。对于在 templatetags 包中放置多少个模块没有做任何的限制。需要了解的是: {% load %} 语句会为指定的Python模块名(而非应用程序名)加载标签或过滤器。
一旦创建了Python模块,你只需根据是要编写过滤器还是标签来相应的编写一些Python代码。
要成为有效的标签库,模块必须包含一个模块级的变量: register ,这是一个 template.Library 的实例。这个template.Library 实例是包含所有已注册的标签及过滤器的数据结构。因此,在模块的顶部位置插入下述代码:
from django import template
register = template.Library()
备注
请阅读Django默认的过滤器和标签的源码,那里有大量的例子。他们分别为: django/template/defaultfilters.py 和django/template/defaulttags.py 。某些应用程序在 django.contrib 中也包含模板库。
创建 register 变量后,你就可以使用它来创建模板的过滤器和标签了。
自定义模板过滤器
自定义过滤器就是有一个或两个参数的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()
当你在定义你的过滤器时,你需要用 Library 实例来注册它,这样就能通过Django的模板语言来使用了:
register.filter('cut', cut)
register.filter('lower', lower)
Library.filter() 方法需要两个参数:
§ 过滤器的名称(一个字串)
§ 过滤器函数本身
如果你使用的是Python 2.4或更新,你可以使用 register.filter() 作为一个装饰器:
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
@register.filter
def lower(value):
return value.lower()
像第二个例子中,如果你不使用 name 参数,那么Django将会使用函数名作为过滤器的名字。
下面是一个完整的模板库的例子,提供了一个 cut 过滤器:
from django import template
register = template.Library()
@register.filter(name='cut')
def cut(value, arg):
return value.replace(arg, '')
自定义模板标签
标签要比过滤器复杂些,标签几乎能做任何事情。
第四章描述了模板系统的两步处理过程:编译和呈现。为了自定义一个这样的模板标签,你需要告诉Django当遇到你的标签时怎样进行这过程。
当Django编译一个模板时,它将原始模板分成一个个 节点 。每个节点都是 django.template.Node 的一个实例,并且具备render() 方法。于是,一个已编译的模板就是 Node 对象的一个列表。
当你调用一个已编译模板的 render() 方法时,模板就会用给定的context来调用每个在它的节点列表上的节点的 render() 方法。所以,为了定义一个自定义的模板标签,你需要明确这个模板标签转换为一个 Node (已编译的函数)和这个node的render() 方法。
在下面的章节中,我们将详细解说写一个自定义标签时的所有步骤。
编写编译函数
当遇到一个模板标签(template tag)时,模板解析器就会把标签包含的内容,以及模板解析器自己作为参数调用一个python函数。这个函数负责返回一个和当前模板标签内容相对应的节点(Node)的实例。
例如,写一个显示当前日期的模板标签:{% current_time %},该标签会根据参数指定的 strftime 格式(参见:http://www.djangoproject.com/r/python/strftime/ )显示当前时间。在继续做其它事情以前,先决定标签的语法是一个好主意。在我们的例子里,该标签将会像这样被使用:
The time is {% current_time "%Y-%m-%d %I:%M %p" %}.
备注
没错, 这个模板标签是多余的,Django默认的 {% now %} 用更简单的语法完成了同样的工作。这个模板标签在这里只是作为一个例子。
这个函数的分析器会获取参数并创建一个 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:
msg = '%r tag requires a single argument' % token.contents[0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[1:-1])
1
其实这儿包含了不少东西:
§ parser 是模板分析器对象,在这个例子中我们没有使用它。
§ token.contents 是包含有标签原始内容的字符串。在我们的例子中,它是 'current_time "%Y-%m-%d %I:%M %p"' 。
§ token.split_contents() 方法按空格拆分参数同时保证引号中的字符串在一起。应该避免使用 token.contents.split()(仅是使用Python的标准字符串拆分),它不够健壮,因为它只是简单的按照 所有 空格进行拆分,包括那些引号引起来的字符串中的空格。
§ 这个函数负责抛出 django.template.TemplateSyntaxError ,同时提供所有语法错误的有用信息。
§ 不要把标签名称硬编码在你的错误信息中,因为这样会把标签名称和你的函数耦合在一起。 token.split_contents()[0] 总会是 是你的标签的名称,即使标签没有参数。
§ 这个函数返回一个 CurrentTimeNode (稍后我们将创建它),它包含了节点需要知道的关于这个标签的全部信息。在这个例子中,它只是传递了参数 "%Y-%m-%d %I:%M %p" 。模板标签开头和结尾的引号使用 format_string[1:-1] 除去。
§ 模板标签编译函数 必须 返回一个 Node 子类,返回其它值都是错的。
编写模板节点
编写自定义标签的第二步就是定义一个拥有 render() 方法的 Node 子类。继续前面的例子,我们需要定义 CurrentTimeNode :
import datetime
class CurrentTimeNode(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
now = datetime.datetime.now()
return now.strftime(self.format_string)
这两个函数( init 和 render )与模板处理中的两步(编译与渲染)直接对应。这样,初始化函数仅仅需要存储后面要用到的格式字符串,而 render() 函数才做真正的工作。
与模板过滤器一样,这些渲染函数应该捕获错误,而不是抛出错误。模板标签只能在编译的时候才能抛出错误。
注册标签
最后,你需要用你的模块 Library 实例注册这个标签。注册自定义标签与注册自定义过滤器非常类似(如前文所述)。实例化一个 template.Library 实例然后调用它的 tag() 方法。例如:
register.tag('current_time', do_current_time)
tag() 方法需要两个参数:
模板标签的名字(字符串)。如果被遗漏的话,将会使用编译函数的名字。
1
编译函数。
和注册过滤器类似,也可以在Python2.4及其以上版本中使用 register.tag 修饰:
@register.tag(name="current_time")
def do_current_time(parser, token):
# ...
@register.tag
def shout(parser, token):
# ...
如果你像在第二个例子中那样忽略 name 参数的话,Django会使用函数名称作为标签名称。
在上下文中设置变量
前一节的例子只是简单的返回一个值。很多时候设置一个模板变量而非返回值也很有用。那样,模板作者就只能使用你的模板标签所设置的变量。
要在上下文中设置变量,在 render() 函数的context对象上使用字典赋值。这里是一个修改过的 CurrentTimeNode ,其中设定了一个模板变量 current_time ,并没有返回它:
class CurrentTimeNode2(template.Node):
def __init__(self, format_string):
self.format_string = format_string
def render(self, context):
now = datetime.datetime.now()
context['current_time'] = now.strftime(self.format_string)
return ''
注意 render() 返回了一个空字符串。 render() 应当总是返回一个字符串,所以如果模板标签只是要设置变量, render() 就应该返回一个空字符串。
你应该这样使用这个新版本的标签:
{% current_time2 "%Y-%M-%d %I:%M %p" %}
<p>The time is {{ current_time }}.</p>
但是 CurrentTimeNode2 有一个问题: 变量名 currenttime 是硬编码的。这意味着你必须确定你的模板在其它任何地方都不使用{{ currenttime }} ,因为 {% current_time2 %} 会盲目的覆盖该变量的值。
一种更简洁的方案是由模板标签来指定需要设定的变量的名称,就像这样:
{% get_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):
now = datetime.datetime.now()
context[self.var_name] = 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:
msg = '%r tag requires arguments' % token.contents[0]
raise template.TemplateSyntaxError(msg)
m = re.search(r'(.*?) as (w+)', arg)
if m:
fmt, var_name = m.groups()
else:
msg = '%r tag had invalid arguments' % tag_name
raise template.TemplateSyntaxError(msg)
if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
msg = "%r tag's argument should be in quotes" % tag_name
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode3(fmt[1:-1], var_name)