Django学习笔记-在视图中使用模板
之前我们在mysite.views中创建了current_datetime视图,内容如下:
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
html = "<html><body>It is now %s.</body></html>" % now
return HttpResponse(html)
用Django模板系统来修改该视图,修改后如下:
from django.template import Template, Context
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
t = Template("<html><body>It is now {{ current_date }}.</body></html>")
html = t.render(Context({'current_date': now}))
return HttpResponse(html)
但是这种方法,模板仍然嵌入在Python代码中,并未真正的实现数据与表现的分离。
于是我们使用将模板置于一个单独的文件中,并且让视图加载该文件来解决此问题。
假设文件保存在/home/zxz/django/mysite/templates/mytemplate.html中的话,代码就会像下面这样:
from django.template import Template, Context
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
# Simple way of using templates from the filesystem.
# This is BAD because it doesn't account for missing files!
fp = open('/home/djangouser/templates/mytemplate.html')
t = Template(fp.read())
fp.close()
html = t.render(Context({'current_date': now}))
return HttpResponse(html)
但是这样做,还是会引入一些问题:
它没有对文件丢失的情况做出处理。 如果文件 mytemplate.html 不存在或者不可读, open() 函数调用将会引发 IOError 异常。
这里对模板文件的位置进行了硬编码。 如果你在每个视图函数都用该技术,就要不断复制这些模板的位置。 更不用说还要带来大量的输入工作!
它包含了大量令人生厌的重复代码。 与其在每次加载模板时都调用 open() 、 fp.read() 和 fp.close() ,还不如做出更佳选择。
为了解决这些问题,Django引入了模板自加载、模板目录的技巧。
模板加载
Django提供了一种使用方便且功能强大的API,用于从磁盘中加载模板,要使用这种方法,需要先将模板的保存位置告诉框架:
settings.py里的TEMPLATE下的DIRS
它默认是一个空元组(tuple),新版本中默认为列表。将用于保存模板的目录添加其中即可。
注意:
在老的Djanog版本中,如果DIRS只包含一个目录,别忘了在该目录后加上个逗号,在最新版本的Django中,已经改为列表,不需要加逗号了;
如果使用的是windows平台,请包含驱动器符号并使用Unix风格的斜杠(/)而不是反斜杠();
建议:
最好的方式是使用绝对路径(即从文件系统根目录开始的目录路径),如果想更灵活一点并减少一些负面干扰,可利用Django配置文件就是Python代码这一点来动态构建TEMPLATE_DIRS的内容:
import os.path
TEMPLATE_DIRS = (
os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'),
)
注意:
这里使用的Python内部变量__file__,该变量被自动设置为代码所在的Python模块文件命名。”os.path.dirname(__file__)”将会获取自身所在的文件,即settings.py所在的目录,然后由os.path.join这个方法将这目录与templates进行连接。如果在windows下,它会智能地选择正确的后向斜杠进行连接,而不是前向斜杠。
运用以上的知识,重写current_datetime:
先是建立templates目录:
/home/zxz/djcode/mysite/templates
然后再该目录下建立current_datetime.html文件,文件内容是:
<html><body>It is now{{ current_date }}.</body></html>
修改视图里的current_datetime:
from django.template.loader import get_template
from django.template import Context
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
t = get_template('current_datetime.html')
html = t.render(Context({'current_date': now}))
return HttpResponse(html)
模板current_datetime.html里的current_date在渲染的时候,会被now代替,效果和之前的是一样的,显示当前时间。
系统是如何确认模板文件在系统里的位置的:
get_templdate()方法会自动为你连接已经设置的TEMPLDATE_DIRS目录和你传入的模板名称参数。在本例中,TEMPLATES_DIRS目录被设置为/home/zxz/djcode/mysite/templates,上面的get_template()调用就会为你找到/home/zxz/djcode/mysite/templates/current_date.html这样一个位置。
如果get_template()找不到给定名称的模板,将会引发一个TemplateDoesNotExist异常。
该异常信息里包含了一块很有用的信息,即调试信息区,这里会显示Django要加载哪个模板、每次尝试出错的原因(eg:文件不存在等)。
render_to_response()
Django提供了一个捷径,可以一次性地载入某个模板文件,渲染它,然后将此作为HttpResponse返回:
django.shortcuts模块中名为render_to_response()的函数
使用render_to_response()重写current_datetime:
from django.shortcuts import render_to_response
import datetime
def current_datetime(request):
now = datetime.datetime.now()
return render_to_response('current_datetime.html', {'current_date': now})
注意:
render_to_response()的第一个参数必须是要使用的模板名称,如果要给定第二个参数,那么该参数必须是为该目标创建Context时所使用的字典。如果不提供第二个参数,render_to_response()使用一个空字典。
loads()
使用python的内建函数loads(),它返回的字典对所有局部变量的名称与值进行映射,因此,改写current_datetime:
def current_datetime(request):
current_date = datetime.datetime.now()
return render_to_response('current_datetime.html', locals())
改写后,没有像之前那样手工指定context字典,而是传入了locals()的值,它囊括了函数执行到该时间点时所定义的一切变量,我们将now变量重命名为current_date,因为那才是模板所预期的变量名称。
注意:
使用locals()要注意,它包括所有的局部变量,它们可能比你想让模板访问的要多,在前例中,locals()还包含了request,对此如何取舍取决你的应用程序。
get_template()中使用子目录
推荐将模板存放在模板目录的子目录中,把模板存放于模板目录的子目录中是件很轻松的事情。只需在调用get_template()时,把子目录名和一条斜杠添加到模板名称之前,eg:
t = get_template('dateapp/current_datetime.html')
由于render_to_response()只是对get_template()的简单封装,你可以对render_to_response()的第一个参数做相同处理,eg:
return render_to_response('dateapp/current_datetime.html', {'current_date': now})
注意:
对子目录的深度没有限制,想要多少层都可以;
windows用户必须使用斜杠而不是反斜杠,因为get_template()假定的是Unix风格的文件名符号约定。
include模板标签
内建模板标签:{% include %}
该标签允许在模板中包含其他的模板的内容,标签的参数是所要包含的模板名称,可以是一个变量,也可以是用单/双引号硬编码的字符串,每当在多个模板中出现相同的代码时,就应该考虑是否要使用{% include %}来减少重复。
下面这两个例子都包含了 nav.html 模板。这两个例子是等价的,它们证明单/双引号都是允许的:
{% include 'nav.html' %}
{% include "nav.html" %}
下面的例子包含了 includes/nav.html 模板的内容:
{% include 'includes/nav.html' %}
下面的例子包含了以变量 template_name 的值为名称的模板内容:
{% include template_name %}
如果{% include %}标签指定的模板没找到,Django将会在下面两个处理方法中选择一个:
如果DEBUG设置为True,你将会在Django错误信息页面看到TemplateDoesNotExist异常;
如果DEBUG设置为False,该标签不会引发错误信息,在标签位置不显示任何东西。
模板继承
在实际应用中,需要用Django模板系统来创建整个HTML页面,这就带来一个常见的web开发问题:
在整个网站中,如何减少共用页面区域(比如站点导航)所引起的重复和冗余代码?
解决方案就是使用服务器端的includes,你可以在HTML页面中使用该指令将一个网页嵌入到另一个中。
上面讲到的{% include %}支持了这种方法。
但是Django提供了更加优雅的策略——模板继承。
模板继承,就是先构造一个基础框架模板,而后在其子模板中对它所包含站点公用部分和定义块进行重载。
还是以current_datetime为例,为其创建一个更加完整的模板来体会一下这种做法:
<!DOCTYPE HTML PUBLIC "‐//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<title>The current time</title>
</head>
<body>
<h1>My helpful timestamp site</h1>
<p>It is now {{ current_date }}.</p>
<hr>
<p>Thanks for visiting my site.</p>
</body>
</html>
这看起来很棒,但如果我们要为第三章的 hours_ahead 视图创建另一个模板会发生什么事情呢?
<!DOCTYPE HTML PUBLIC "‐//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<title>Future time</title>
</head>
<body>
<h1>My helpful timestamp site</h1>
<p>In {{ hour_offset }} hour(s), it will be {{ next_time }}.</p>
<hr>
<p>Thanks for visiting my site.</p>
</body>
</html>
很明显,我们刚才重复了大量的 HTML 代码。 想象一下,如果有一个更典型的网站,它有导航条、样式表,可能还有一些 JavaScript 代码,事情必将以向每个模板填充各种冗余的 HTML 而告终。
解决这个问题的服务器端 include 方案是找出两个模板中的共同部分,将其保存为不同的模板片段,然后在每个模板中进行 include。 也许你会把模板头部的一些代码保存为 header.html 文件:
<!DOCTYPE HTML PUBLIC "‐//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
把底部保存到文件 footer.html :
<hr>
<p>Thanks for visiting my site.</p>
</body>
</html>
对基于 include 的策略,头部和底部的包含很简单。 麻烦的是中间部分。 在此范例中,每个页面都有一个
<h1>My helpful timestamp site</h1>
标题,但是这个标题不能放在 header.html 中,因为每个页面的
<title>
是不同的。 如果我们将<h1>
包含在头部,我们就不得不包含<title>
,但这样又不允许在每个页面对它进行定制。何去何从呢?
Django的模板继承系统解决了这些问题。你可以将其视为服务器端 include 的逆向思维版本。 你可以对那些不同的代码段进行定义,而不是共同代码段。
第一步是定义基础模板, 该框架之后将由子模板所继承。 以下是我们目前所讲述范例的基础模板:
<!DOCTYPE HTML PUBLIC "‐//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<h1>My helpful timestamp site</h1>
{% block content %}{% endblock %}
{% block footer %}
<hr>
<p>Thanks for visiting my site.</p>
{% endblock %}
</body>
</html>
这个叫做base.html的模板定义了一个简单的HTML框架文档,我们将在本站点的所有页面中使用。子模板的作用就是重载、添加或保留那些块的内容。
我们使用一个以前已经见过的模板标签:{% block %}。 所有的{% block %}标签告诉模板引擎,子模板可以重载这些部分。每个{% block %}标签所要做的是告诉模板引擎,该模板下的这一块内容将有可能被子模板覆盖。
看起来很漂亮是不是?每个模板只包含对自己而言独一无二的代码。无需多余的部分。如果想进行站点级的设计修改,仅需修改base.html,所有其它模板会立即反映出所作修改。
工作流程:
在加载current_datetime.html模板时,模板引擎发现了{% extends %}标签,注意到该模板是一个子模板。模板引擎立即装载其父模板,即本例中的base.html。
此时,模板引擎注意到base.html中的三个{% block %}标签,并用子模板的内容替换这些block。因此,引擎将会使用我们在{ block title %}中定义的标题,对{% block content %}也是如此。所以,网页标题一块将由{% block title %}替换,同样地,网页的内容一块将由{% block content %}替换。
注意由于子模板并没有定义footer块,模板系统将使用在父模板中定义的值。
继承最常见的三层法:
创建base.html模板,在其中定义站点的主要外观感受,这些都是不常修改甚至从不修改的部分;
为网站的每个区域创建base_SECTION.html模板(eg:base_photos.html和base_forum.html),这些模板对base.html进行拓展,并包含区域特定的风格与设计;
为每种类型的页面创建独立的模板,例如论坛页面或者图片库,这些模板拓展相应的区域模板。
使用模板继承的诀窍:
如果在模板中使用{% extends %},必须保证其为模板中的第一个模板标记,否则,模板继承将不起作用;
一般来说,基础模板中的{% block %}标签越多越好,记住,子模板不必定义父模板中所有的代码块,因此你可以用合理的缺省值对一些代码块进行填充,然后只对子模板所需的代码块进行(重)定义。俗话说,钩子越多越好;
如果发觉自己在多个模板之间拷贝代码,你应该考虑将该代码段放置到父模板的某个{% block %}中;
如果你需要访问父模板中的块的内容,使用{{ block.super }}这个标签吧,这一个魔法变量将会表现出父模板中的内容。如果只想在上级代码块基础上添加内容,而不是全部重载,该变量就显得非常有用了;
不允许在同一个模板中定义多个同名的{% block %}。 存在这样的限制是因为block标签的工作方式是双向的。也就是说block标签不仅挖了一个要填的坑,也定义了在父模板中这个坑所填充的内容。如果模板中出现了两个相同名称的{% block %}标签,父模板将无从得知要使用哪个块的内容;
{% extends %}对所传入模板名称使用的加载方法和get_template()相同。也就是说,会将模板名称被添加到TEMPLATE_DIRS 设置之后;
多数情况下,{% extends %}的参数应该是字符串,但是如果直到运行时方能确定父模板名,这个参数也可以是个变量。这使得你能够实现一些很酷的动态功能;