您已经学习了使用模板系统的基础知识; 现在让我们用这个知识来创建一个视图。
回想一下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代码中,所以没有实现数据和表示的真正分离。 让我们通过把模板放在一个单独的文件中来解决这个问题。
您可能首先考虑将模板保存在文件系统的某个位置,并使用Python的内置文件打开功能来读取模板的内容。 假设模板保存为\users\djangouser\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('\users\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提供了一个方便而强大的API来从文件系统加载模板,目的是在模板加载调用和模板本身中删除冗余。 为了使用这个模板加载API,首先你需要告诉框架存储模板的地方。 做这个的地方在你的设置文件中 - 我上一章提到的settings.py文件,当我介绍ROOT_URLCONF设置时。 如果你跟着,打开你的settings.py并找到模板设置。 这是一个配置列表,每个引擎一个:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
# ... some options here ...
},
},
]
BACKEND是实现Django模板后端API的模板引擎类的虚线Python路径。 内置的后端是django.template.backends.django.DjangoTemplates和django.template.backends.jinja2.Jinja2。
由于大多数引擎从文件加载模板,因此每个引擎的顶级配置包含三个常用设置:
- DIRS根据搜索顺序定义引擎应该查找模板源文件的目录列表。
- APP_DIRS告诉引擎是否应该在已安装的应用程序中查找模板。 按照惯例,当APPS_DIRS设置为True时,DjangoTemplates将在每个INSTALLED_APPS中查找\ templates子目录。 这允许模板引擎即使DIRS为空也能找到应用程序模板。
- OPTIONS包含后端特定的设置。
模板目录
DIRS默认情况下是一个空的列表。 为了告诉Django的模板加载机制在哪里寻找模板,选择一个你想存储你的模板的目录,并将其添加到DIRS,如下所示:
'DIRS': [
'/home/html/templates/site',
'/home/html/templates/default',
],
有几件事要注意:
-
除非你正在构建一个没有应用程序的简单程序,否则最好把DIRS留空。 默认设置文件将APP_DIRS配置为True,所以你最好在你的Django应用程序中有一个“templates”子目录。
-
如果您想在项目根目录下拥有一组主模板,例如 mysite \ templates,你需要设置DIRS,如下所示:
'DIRS': [os.path.join(BASE_DIR, 'templates')],
-
您的模板目录不必被称为模板 - Django不会对您使用的名称进行任何限制 - 但是如果您坚持使用惯例,它会使您的项目结构更容易理解。如果您不想使用 默认的,或不能由于某种原因,你可以指定你想要的任何目录,只要该目录中的目录和模板可以被运行你的Web服务器的用户帐户读取。
-
如果您在Windows上,请包含您的驱动器盘符,并使用Unix风格的正斜杠而非反斜杠,如下所示:
'DIRS':
[
'C:/www/django/templates',
]
由于我们还没有创建一个Django应用程序,我将使用一个非常简单的配置来演示如何加载模板。
首先,你必须按照上面的例子设置DIRS到[os.path.join(BASE_DIR,'templates')]。 你的设置文件现在应该是这样的:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR,'templates')],
'APP_DIRS': True,
'OPTIONS': {
# ...
设置DIRS后,接下来要在root \ mysite文件夹中创建一个\ templates目录。 完成后,文件夹结构应如下所示:
\mysite_project
\mysite
\mysite
\templates
manage.py
下一步是将视图代码更改为使用Django的模板加载功能,而不是对模板路径进行硬编码。 回到我们的current_datetime视图,让我们改变它:
#mysite\mysite\views.py
from django.template.loader import get_template
from django.http import HttpResponse
import datetime
def current_datetime(request):
now = datetime.datetime.now()
t = get_template('current_datetime.html')
html = t.render({'current_date': now})
return HttpResponse(html)
通常,您将使用Django模板加载器,而不是像前面的示例中那样使用低级模板API。 在这个例子中,我们使用函数django.template.loader.get_template()而不是手动从文件系统加载模板。 get_template()函数将模板名称作为其参数,计算模板在文件系统上的位置,打开该文件并返回编译后的Template对象。
还要注意这里的render()方法和前面的例子之间的区别。 在前面的例子中,我们调用了django.template.Template.render,它要求你传入一个Context对象。 get_template()从django.template.backends.base.Template返回一个依赖于后端的Template,其中render()方法只接受一个字典对象,而不接受一个Context对象。
我们在这个例子中的模板是current_datetime.html,但没有什么特别的.html扩展名。 你可以给你的模板任何扩展名对你的应用程序有意义,或者你可以完全忽略扩展名。
要确定文件系统上模板的位置,get_template()将按顺序查找:
-
如果APP_DIRS设置为True,并且假设您正在使用DTL,则会在当前应用程序中查找/ templates目录。
-
如果它没有在当前应用程序中找到您的模板,get_template()会将您的DIRS模板目录与传递给get_template()的模板名称相结合,并依次遍历每个模板直到找到您的模板。 例如,如果DIRS中的第一个条目设置为'/ home / django / mysite / templates',则上面的get_template()调用将查找模板/home/django/mysite/templates/current_datetime.html。
-
如果get_template()无法找到具有给定名称的模板,则会引发TemplateDoesNotExist异常。
Windows用户
一定要使用正斜杠而不是反斜杠。 get_template()假定一个Unix风格的文件名称。
要查看模板异常的样子,请在Django项目的目录中运行python manage.py runserver
,再次启动Django开发服务器。 然后,将浏览器指向激活current_datetime视图的页面(例如http://127.0.0.1:8000/time/)。 假设你的DEBUG设置被设置为True,并且你还没有创建一个current_datetime.html模板,你应该看到一个Django错误页面,突出显示了TemplateDoesNotExist错误(图3-1)。
这个错误页面与我在第2章中解释的错误页面类似,另外还有一个调试信息:“Template-loader postmortem”部分。 本节将告诉您Django尝试加载哪些模板以及每次尝试失败的原因(例如,“文件不存在”)。 当您尝试调试模板加载错误时,此信息是无价的。
要解决我们缺少的模板问题,我们使用以下模板代码创建current_datetime.html文件:
# \mysite_project\mysite\templates\current_datetime.html
<html>
<body>
It is now {{ current_date }}
</body>
</html>
It is now {{ current_date }}.
将此文件保存到您以前创建的\ mysite \ templates目录中。 刷新网页浏览器中的页面,你会看到完全呈现的页面(图3-2)。
图3-2:Django现在可以正确地呈现模板.png
render()
到目前为止,我已经向您展示了如何加载一个模板,填充一个Context,并返回一个HttpResponse对象和渲染模板的结果。 下一步是优化它使用get_template()而不是硬编码模板和模板路径。
Django的开发人员认识到,因为这是一个常见的习惯用法,所以Django需要一个可以在一行代码中完成所有这些工作的捷径。 这个快捷方式是一个名为render()的函数,它位于模块django.shortcuts中。
大多数情况下,您将使用render()而不是加载模板,并手动创建Context和HttpResponse对象 - 但是,记住完整的流程是有用的,因为它适用于某些非标准的用例。
以下是正在进行的current_datetime示例,重写为使用render():
from django.shortcuts import render
import datetime
def current_datetime(request):
now = datetime.datetime.now()
return render(request, 'current_datetime.html', {'current_date': now})
有什么区别! 我们来看看代码上的变化:
-
我们不再需要导入get_template,Template,Context或HttpResponse。 相反,我们导入django.shortcuts.render。 导入日期时间保持不变。
-
在current_datetime函数中,我们现在仍在计算,但是模板加载,上下文创建,模板渲染和HttpResponse创建都由render()调用来处理。 因为render()返回一个HttpResponse对象,所以我们可以直接在视图中返回这个值。
render()的第一个参数是请求,第二个参数是要使用的模板的名称。 第三个参数(如果给出的话)应该是一个字典,用来为该模板创建一个Context。 如果你不提供第三个参数,render()将使用一个空的字典。
模板子目录
将所有模板存储在单个目录中可能会很困难。 您可能希望将模板存储在模板目录的子目录中,这很好。
实际上,我建议这样做; 一些更高级的Django特性(比如我们在第10章介绍的通用视图系统)将这种模板布局视为默认的约定。 它还为您的模板提供了自己的命名空间,在我们开始构建Django应用程序时,我们将在本书的后面探讨它的用途。
将模板存储在模板目录的子目录中很容易。 在对get_template()的调用中,只需在模板名称之前包含子目录名称和斜线,如下所示:
t = get_template('dateapp/current_datetime.html')
因为render()是get_template()的一个小包装,所以你可以用render()的第二个参数做同样的事情,就像这样:
return render(request, 'dateapp/current_datetime.html', {'current_date': now})
子目录树的深度没有限制。 随意使用尽可能多的子目录,只要你喜欢。 与本章中的所有其他方法一样,Windows用户必须记住在路径名中使用正斜杠(/),而不是反斜杠(\)。
包含模板标签
现在我们已经介绍了模板加载机制,我们可以引入一个内置的模板标签来利用它:{%include%}。
这个标签允许你包含另一个模板的内容。 标签的参数应该是要包含的模板的名称,模板名称可以是变量,也可以是硬编码(带引号)的字符串,可以是单引号或双引号。
只要您在多个模板中使用相同的代码,请考虑使用{%include%}来删除重复项。 这两个示例包括模板nav.html的内容。 这些例子是等价的,说明可以使用单引号或双引号:
{% include 'nav.html' %}
{% include "nav.html" %}
此示例包含模板includes / nav.html的内容:
{% include 'includes/nav.html' %}
此示例包含名称包含在变量template_name中的模板的内容:
{% include template_name %}
{%include%}标签也允许相对路径:
{% include './nav.html' %}
{% include '../nav_base.html' %}
与get_template()中一样,模板的文件名是通过将当前Django应用程序中的\ templates目录的路径(如果APPS_DIR为True)或通过将DIRS中的模板目录添加到请求的模板名称来确定的。 包含的模板将使用包含它们的模板的上下文进行评估。
例如,考虑这两个模板:
# mypage.html
<html>
<body>
{% include "includes/nav.html" %}
<h1>{{ title }}</h1>
</body>
</html>
# includes/nav.html
You are in: {{ current_section }}
如果使用包含current_section的上下文呈现mypage.html,则该变量将在“included”模板中可用,如您所期望的那样。
如果在{%include%}标记中找不到具有给定名称的模板,Django将执行以下两项操作之一:
-
如果DEBUG设置为True,你会在Django错误页面上看到TemplateDoesNotExist异常。
-
如果DEBUG设置为False,则标签将自动失败,在标签处没有显示任何内容。
包含的模板之间没有共享状态 - 每个包含都是完全独立的呈现过程。 在包含块之前对它们进行评估。 这意味着包含另一个块的模板将包含已经被评估和渲染的块 - 而不是可以被例如扩展模板覆盖的块。
模板的继承
到目前为止,我们的模板示例都是微小的HTML片段,但在现实世界中,您将使用Django的模板系统来创建整个HTML页面。这导致了一个共同的Web开发问题:在一个网站上,如何减少常见页面区域的重复和冗余,如全站导航?
解决这个问题的一个经典的方法是使用服务器端包含,你可以在HTML页面中嵌入指令来将一个网页“包含”在另一个网页中。事实上,Django支持这种方法,刚刚描述了{%include%}模板标记。
但是用Django解决这个问题的首选方法是使用一种更优雅的策略,即模板继承。实质上,模板继承允许您构建一个基本“骨架”模板,其中包含站点的所有常见部分,并定义子模板可以覆盖的“块”。让我们通过编辑current_datetime.html文件,为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视图? 如果我们想再次制作一个很好的,有效的,完整的HTML模板,我们会创建如下所示的内容:
<!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放入每个模板。
服务器端解决这个问题的方法是将两个模板中的常见比特分解出来,并将它们保存在单独的模板片段中,然后将其包含在每个模板中。 也许你会将模板的最高位保存在一个名为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>
采用基于包含的策略,页眉和页脚很容易。 这是混乱的中间地带。 在这个例子中,两个页面都有一个标题 - “我的有用的时间戳站点” - 但是这个标题不适合header.html,因为这两个页面的标题是不同的。 如果我们在标题中包含<h1>,我们必须包含标题,这不允许我们在每个页面上定制它。
Django的模板继承系统解决了这些问题。 您可以将其视为服务器端包含的“内外”版本。 不是定义常见的片段,而是定义不同的片段。
第一步是定义一个基本模板 - 子模板稍后将填充的页面骨架。下面是我们正在进行的示例的基本模板:
<!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骨架文档,我们将把这个磨板使用到该网站的所有页面。
这是儿童模板的工作,可以覆盖或添加或单独保留块的内容。 (如果你正在跟着,把这个文件保存到你的模板目录中,如base.html。)
我们在这里使用了一个您以前从未见过的模板标签:{%block%}标签。 所有的{%block%}标签都会告诉模板引擎一个子模板可能会覆盖模板的那些部分。
现在我们有了这个基础模板,我们可以修改我们现有的current_datetime.html模板来使用它:
{% extends "base.html" %}
{% block title %}The current time{% endblock %}
{% block content %}
<p>It is now {{ current_date }}.</p>
{% endblock %}
我们来看第3章的hours_ahead视图。(如果你跟着代码,我会留下来让你改变hours_ahead来使用模板系统而不是硬编码 HTML。)以下是可能的样子:
{% extends "base.html" %}
{% block title %}Future time{% endblock %}
{% block content %}
<p>
In {{ hour_offset }} hour(s), it will be {{ next_time }}.
</p>
{% endblock %}
这不美吗?每个模板只包含该模板特有的代码。不需要冗余。如果您需要进行网站范围内的设计更改,只需对base.html进行更改,其他所有模板都会立即反映更改。
这是如何工作的。当您加载模板current_datetime.html时,模板引擎会看到{%extends%}标记,注意这个模板是一个子模板。引擎立即加载父模板 - 在这种情况下,base.html。
此时,模板引擎会注意到base.html中的三个{%block%}标记,并将这些块替换为子模板的内容。因此,我们在{%block title%}中定义的标题将被使用,{%block content%}也将被使用。
请注意,由于子模板未定义页脚块,因此模板系统将使用父模板中的值。父模板中{%block%}标记中的内容始终用作回退。
继承不会影响模板上下文。换句话说,继承树中的任何模板都可以访问上下文中的每一个模板变量。您可以根据需要使用尽可能多的继承级别。使用继承的一个常见方法是以下三层方法:
-
创建一个base.html模板,该模板包含您网站的主要外观。 这是很少有变化的东西。
-
为您的网站的每个“部分”(例如,base_photos.html和base_forum.html)创建一个base_SECTION.html模板。 这些模板扩展了base.html并包含了特定部分的样式/设计。
-
为每种类型的页面创建单独的模板,例如论坛页面或照片库。 这些模板扩展了适当的部分模板。
此方法最大限度地实现了代码重用,并使项目可以轻松地添加到共享区域,例如部分区域导航。
以下是使用模板继承的一些准则:
-
如果您在模板中使用{%extends%},那么它必须是该模板中的第一个。
否则,模板继承将不起作用。 -
通常,基本模板中的{%block%}标记越多越好。请记住,子模板不必定义所有父块,因此可以用合理的默认值填充合理数量的块,并仅在子模板中定义所需的值。有更多的钩子比更少的钩子更好。
-
如果您发现自己从大量模板中复制代码,则可能意味着您应该将代码移动到父模板中的{%block%}。
-
如果您需要从父模板获取块的内容,请使用{{block.super}},这是一个“魔术”变量,用于提供父模板的渲染文本。如果要添加父块的内容而不是完全覆盖它,这非常有用。
-
您无法在同一模板中定义多个具有相同名称的{%block%}标记。这个限制的存在是因为块标签在“两个”方向上工作。也就是说,块标记不仅提供了一个要填充的孔,还定义了填充父项的孔的内容。如果模板中有两个具有相似名称的{%block%}标记,则模板的父级不知道要使用哪个块的内容。
-
传递给{%extends%}的模板名称使用get_template()所使用的相同方法加载。也就是说,模板名称会附加到您的DIRS设置或您当前的Django应用程序的\ templates文件夹中。
-
在大多数情况下,{%extends%}的参数将是一个字符串,但如果您在运行时不知道父模板的名称,它也可能是一个变量。这可以让你做一些很酷和动态的事情。
下一步是什么?
你现在已经掌握了Django模板系统的基础知识。 下一步是什么? 大多数现代网站都是数据库驱动的:网站的内容存储在关系数据库中。 这允许数据和逻辑的清晰分离(与视图和模板相同,可以分离逻辑和显示)。下一章介绍了Django为您提供的与数据库交互的工具。
网友评论