一、模板
Web框架把我们从WSGI中拯救出来了。现在,我们只需要不断地编写函数,带上URL,就可以继续Web App的开发了。
但是,Web App不仅仅是处理逻辑,展示给用户的页面也非常重要。在函数中返回一个包含HTML的字符串,简单的页面还可以,但是,想想新浪首页的6000多行的HTML,你确信能在Python的字符串中正确地写出来么?
俗话说得好,不懂前端的Python工程师不是好的产品经理。有Web开发经验的同学都明白,Web App最复杂的部分就在HTML页面。HTML不仅要正确,还要通过CSS美化,再加上复杂的JavaScript脚本来实现各种交互和动画效果。总之,生成HTML页面的难度很大。
由于在代码里拼字符串是不现实的,所以,模板技术出现了。在Python中,较为火的模板就是Jinja2了(Flask支持Jinja2),而Django Web框架中也帮我们内置好了模板(MTV中的T),和jinja2很像,使用简单。
Django模板是一个简单的文本文档,或用Django模板语言标记的一个Python字符串。 某些结构是被模板引擎解释和识别的,主要的有变量和标签。模板是由context来进行渲染的。渲染的过程是用在context中找到的值来替换模板中相应的变量,并执行相关tags。其他的一切都原样输出。
Django模板语言的语法包括四个结构。
- 变量
下面我们先在Django shell下简单测试一下Django模板的一个使用:
1 2 3 4 5 6 |
$ python manage.py shell >>> from django.template import Template, Context >>> t = Template('My name is {{ name }}') #Template就是支持标签的字符串; >>> c = Context({'name': 'dkey'}) #Context就是一个字典,用来渲染Template; >>> t.render(c) #最后就是调用Template的方法来进行模板的一个渲染,得到一个结果; 'My name is dkey' |
变量是被{{ 和 }}括起来的部分。变量的值是来自context中的输出, 这类似于字典对象的keys到values的映射关系。
需要特殊说明的一点是Template中带点的标签,比如“{{ Person.name }}”,其中这个点代表的含义比较多,比如:字典查找、属性查询、方法查询和列表查询。
1. 字典查询
1 2 3 4 |
>>> t = Template('My name is {{ Person.name }}') >>> person = {'name':'dkey'} >>> t.render(Context({'Person': person})) 'My name is dkey' |
2. 属性查询
1 2 3 4 5 6 7 8 |
>>> t = Template('My name is {{ Person.name }}') >>> class Person: ... def __init__(self, name): ... self.name = name ... >>> Person = Person('dkey') >>> t.render(Context({'Person': person})) 'My name is dkey' |
3. 方法查询
如果一个变量被解析为一个可调用的,模板系统会调用它不带任何参数,并使用调用它的结果来代替这个可调用对象本身。
1 2 3 4 5 6 7 8 |
>>> t = Template('My name is {{ Person.name }}') >>> class Person: ... def name(self): ... return 'dkey' ... >>> person = Person() >>> t.render(Context({'Person': person})) 'My name is dkey' |
4. 列表/元祖查询
1 2 3 4 |
>>> t = Template('My name is {{ Person.0 }}') >>> person = ['dkey','jerry'] >>> t.render(Context({'Person': person})) 'My name is dkey' |
- 标签
标签在渲染的过程中提供任意的逻辑。这个定义是刻意模糊的。例如,一个标签可以输出内容,作为控制结构,例如“if”语句或“for”循环从数据库中提取内容,甚至可以访问其他的模板标签。
1. for循环
迭代显示列表,字典等中的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 语法; {% for person in person_list %} <li> {{ person.name }} </li> {% endfor %} # 样例; >>> t = Template(''' ... {% for person in person_list %} ... <li> {{ person.name }} </li> ... {% endfor %} ... ''') >>> person = [{'name':'dkey'},{'name':'jerry'}] >>> t.render(Context({'person_list': person})) '\n\n<li> dkey </li>\n\n<li> jerry </li>\n\n' |
2. if判断
判断是否显示该内容,比如判断是手机访问,还是电脑访问,给出不一样的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 语法; {% if value > 60 %} <li> yes: {{ value }} </li> {% else %} <li> no: {{ value }} </li> {% endif %} # 样例; >>> t = Template(''' ... {% if value >= 60 %} ... <li> yes: {{ value }} </li> ... {% else %} ... <li> no: {{ value }} </li> ... {% endif %} ... ''') >>> t.render(Context({'value': 60})) '\n\n<li> yes: 60 </li>\n\n' |
- 过滤器
过滤器会更改变量或标签参数的值。管道符号后面的功能,比如{{ var | length }},求变量长度的length就是一个过滤器。又比如时间格式也没有必要在view中处理,可以直接在模板中格式化,以至于可以更加灵活。可以简单把过滤器理解为一个函数,而变量当做参数传递给函数处理,然后返回处理结果。
1 2 3 4 5 6 7 8 |
# 语法; {{ now | date:"Y-m-d" }} {{ name | length }} # 样例; >>> t = Template('My name length is {{ name | length }}') >>> t.render(Context({'name': 'dkey'})) 'My name length is 4' |
具体可以查看内置过滤器参考和开发自定义过滤器指南这两篇文档。
- 注释
注释看起来像这样:
1 |
{# this won't be rendered #} |
在{% comment %}和{% endcomment %}之间的内容会被忽略。作为注释,在第一个标签可以插入一个可选的记录。 比如,当要注释掉一些代码时,可以用此来记录代码被注释掉的原因。
1 2 3 4 |
<p>Rendered text with {{ pub_date|date:"c" }}</p> {% comment "Optional note" %} <p>Commented out text with {{ create_date|date:"c" }}</p> {% endcomment %} |
comment标签不能嵌套使用。
二、View中使用模板
在前面的学习中我们都是用简单的django.http.HttpResponse来把内容显示到网页上,下面介绍如何使用渲染模板的方法来显示内容。
使用模板,我们需要预先准备一个HTML文档,这个HTML文档不是普通的HTML,而是嵌入了一些变量和指令,然后,根据我们传入的数据,替换后,得到最终的HTML,发送给用户即可。
Django默认会去app/templates目录下寻找模板,这是settings中的默认设置,默认会去app/static目录寻找静态文件(css,js,jpg)。但是需要注意的是Django默认寻找模板的方式是会从所有App下的templates目录下一个个匹配,所以如果有多个App,并且默认主页都是index.html,可能就会出现不同的结果,默认哪个先找到就会先用哪个。为了避免这个问题,创建模板时,较为规范的写法就是这样建立模板目录/app/templates/app/index.html,这样每个App由于名字不同,所以不会产生相同的模板。
下面我们完全按照MTV设计模式在polls应用下分别写各自的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# ./polls/templates/polls/index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Python Django Template</title> </head> <body> <ul> {% for question in latest_question_list %} <li> <a href="/polls/" > {{ question.question_text }} </a></li> {% endfor %} </ul> </body> </html> # ./polls/views.py from django.shortcuts import render from django.http import HttpResponse from polls.models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:3] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) # ./polls/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$', views.index, name='index'), ] |
简单说一下render函数,结合一个给定的模板和一个给定的上下文字典,并返回一个渲染后的 HttpResponse 对象。
render(request, template_name, context=None, content_type=None, status=None, using=None)
必选参数:
request:就是传入request对象,用户请求信息,用于生成response。
template_name:模板文件名称。
可选参数:
context:添加到渲染模板的一个上下文字典,默认是{}。
content_type:生成的文档要使用的MIME类型,默认为DEFAULT_CONTENT_TYPE设置的值,为text/html。也可以返回为application/json,浏览器接收到类型后进行处理。
status:返回的状态码,默认是200。
using:定义模板引擎的,可以使用多个引擎,但需要在settings里面配置。
我们在View里定义了一个index函数,然后从数据库取出最近发布的三个问题,取出的结果是一个QuerySet可迭代对象,然后传给render函数渲染。
服务器启动正常的情况下,访问http://10.10.0.109:8000/polls/结果如下:
四、公用模板
模板引擎通过TEMPLATES设置来配置,它是一个设置选项列表,与引擎一一对应。默认的值为空。由startproject命令生成的settings.py定义了一些有用的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] |
BACKEND是一个指向实现了Django模板后端API的模板引擎类的带点的Python路径,内置的后端有django.template.backends.django.DjangoTemplates和django.template.backends.jinja2.Jinja2。
由于绝大多数引擎都是从文件加载模板的,所以每种模板引擎都包含两项通用设置:
- DIRS定义了一个目录列表,模板引擎按列表顺序搜索这些目录以查找模板源文件,默认为空表示当前应用目录。
- APP_DIRS告诉模板引擎是否应该进入每个已安装的应用中查找模板,默认为true,表示会到所有App中依次查找。每种模板引擎后端都定义了一个惯用的名称作为应用内部存放模板的子目录名称。(注:例如django为它自己的模板引擎指定的是 ‘templates’ ,为jinja2指定的名字是‘jinja2’)。
特别的是,django允许你有多个模板引擎后台实例,且每个实例有不同的配置选项。在这种情况下你必须为每个配置指定一个唯一的NAME。OPTIONS中包含了具体的backend设置。
另外当我们有多个App时,可能会有多个App公用一个templates和static,这种情况下,我们给每个App都设置一个templates和static显然是不必要的。那么我们就可以设置一个公用templates和static,如下:
1 |
'DIRS': [os.path.join(BASE_DIR, 'templates')] |
BASE_DIR变量表示我们的项目目录,在settings文件中有默认设置BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))。表示我们在项目目录下创建一个templates目录作为公共模板,一般做法也是这样。
那么对于静态文件,也有App默认搜索路径和公共搜索路径,如下:
1 2 3 4 5 6 7 |
# 定义App默认搜索路径; STATIC_URL = '/static/' # 定义公共搜索路径; STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static') ] |
然后你可以在index.html把图片加载进来,添加如下一行:
1 |
<img src="/static/django.png" alt="photo" /> |
然后访问链接应该就可以把图片加载出来了(当然目录创建和图片上传需自行完成)。
对于static资源的访问,可以使用{% load static %}标签,方便settings改配置static,如下配置跟上面写绝对路径效果一样。
1 2 |
{% load static %} <img src="{% static "django.png" %}" alt="photo" /> |
你可能会看到这个标签一般都会用在加载css、js文件,如下:
1 2 3 |
{% load static %} <link rel="stylesheet" href="{% static "css/register.css" %}"/> <script src="{% static 'js/jquery.min.js' %}"></script> |
可能有些地方会看见使用{% load staticfiles %},也是一样可以加载。
五、URL反解析
在上面的urls.py文件中我们使用到了url函数的name参数,如下:
1 2 3 |
urlpatterns = [ url(r'^$', views.index, name='index') ] |
这里的name=’index’是用来干什么的呢?
简单说,name可以用于在templates, models, views ……中得到对应的网址,相当于“给网址取了个名字”,只要这个名字不变,网址变了也能通过名字获取到。我们称之为“URL反解析”。
比如我们在开发的时候,刚开始想用的是/polls/访问,后台发现这样不好,比如我们又想改成/new_polls/这样的形式,但是我们在网页中,代码中很多地方都写的是/polls/这样的形式,这样就导致我们在每个地方都要改,修改网址的代价很大。
有没有更优雅的方式来解决这个问题呢?当然答案是肯定的。我们先说一下如何用Python代码获取对应的网址:
1 2 3 4 |
$ python manage.py shell >>> from django.shortcuts import reverse # django1.10.x新曾,更加规范了; >>> reverse('index') '/polls/' |
reverse函数接收url中的name值作为第一个参数,后面可以通过args=(参数1,参数2)来传入参数,拼装成一个URL。我们在代码中就可以通过reverse()来获取对应的网址(这个网址可以用来跳转,也可以用来计算相关页面的地址),只要对应的url的name不改,就不用改代码中的网址。
在Django模板中使用也是一样,可以很方便的使用:
不带参数的:{% url ‘name’ %}
带参数的,参数可以是变量名:{% url ‘name’ 参数 %}
例如:<a href=”{% url ‘plus’ 100 200 %}”>link</a>,这样就可以通过{% url ‘plus’ 100 200 %} 获取到对应的网址/plus/100/200/。
前面我们在index.html中做了一个超链接,是一个硬编码的链接地址,如下:
1 |
<li> <a href="/polls/" > {{ question.question_text }} </a></li> |
现在我们借助反解析,把硬编码地址改为动态的,如下:
1 |
<li> <a href="{% url 'index' %}" > {{ question.question_text }} </a></li> |
那么此时就算把./project_name/urls.py文件中的url(r’^polls/’, include(‘polls.urls’))换成url(r’^new_polls/’, include(‘polls.urls’))后,模板中会自动反解析出新的超链接地址,无需人为改动,这就是超链接的好处。
开始可能觉得直接写网址简单,但是用多了你一定会发现,用“死网址”的方法很糟糕。
六、URL命名空间
知道了URL反解析之后,又可以考虑另外一个问题了,那就是如果多个App使用同样的一个name值怎么办呢?此时反解析时就不知道找哪个name。所以Django又为我们设计了URL命名空间的概念。
对于URL命名空间有两种级别,一种是App级别的命名空间,一种是Instance级别的命名空间。这里就简单说一下App级别的命名空间,处理方法也很简单,在urls.py文件中定义一个变量即可:
1 2 3 |
# ./polls/urls.py ... app_name = 'polls' |
或者写成下面这种形式:
1 2 3 |
# ./project_name/urls.py ... url(r'^polls/', include('polls.urls', app_name='polls')), |
那么在使用反解析时就得带上命名空间了,不然就会报错的:
1 2 3 |
>>> from django.shortcuts import reverse >>> reverse('polls:index') '/polls/' |
同理,在模板中也得改,所以最好一开始都定义好App namespace,免得日后需要更改。
对于instance namespace一般用的不多,只有在你的App有多个include时则使用instance namespace(在urls中同一个App默认只能由一个include)。设置方法如下:
1 2 3 |
# ./project_name/urls.py ... url(r'^polls/', include('polls.urls'), namespace='polls') |
六、模版继承
网站模板的设计,一般的,我们做网站有一些通用的部分,比如导航,底部,访问统计代码等等。
Django模版引擎中最强大也是最复杂的部分就是模版继承了,模版继承可以让您创建一个基本的“骨架”模版,它包含您站点中的全部元素,并且可以定义能够被子模版覆盖的blocks 。
通过从下面这个例子开始,可以容易的理解模版继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!DOCTYPE html> <span class="hl-brackets"><</span><span class="hl-reserved">meta</span> <span class="hl-var">charset</span><span class="hl-code">=</span><span class="hl-quotes">"</span><span class="hl-string">utf-8</span><span class="hl-quotes">"</span><span class="hl-brackets">></span> <head> <title>{% block title %}My amazing site{% endblock %}</title> </head> <body> <div id="sidebar"> {% block sidebar %} <ul> <li><a href="/">Home</a></li> <li><a href="/blog/">Blog</a></li> </ul> {% endblock %} </div> <div id="content"> {% block content %}{% endblock %} </div> </body> </html> |
这个模版,我们把它叫作base.html, 它定义了一个可以用于两列排版页面的简单HTML骨架。“子模版”的工作是用它们的内容填充空的blocks。
在这个例子中, block标签定义了三个可以被子模版内容填充的block。 block告诉模版引擎: 子模版可能会覆盖掉模版中的这些位置。
子模版可能看起来是这样的:
1 2 3 4 5 6 7 8 9 10 |
{% extends "base.html" %} {% block title %}My amazing blog{% endblock %} {% block content %} {% for entry in blog_entries %} <h2>{{ entry.title }}</h2> <p>{{ entry.body }}</p> {% endfor %} {% endblock %} |
“extends”标签是这里的关键,它告诉模版引擎,这个模版“继承”了另一个模版。当模版系统处理这个模版时,首先,它将定位父模版——在此例中,就是“base.html”。那时,模版引擎将注意到base.html中的三个block标签,并用子模版中的内容来替换这些block。根据blog_entries的值,输出可能看起来是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!DOCTYPE html> <span class="hl-brackets"><</span><span class="hl-reserved">meta</span> <span class="hl-var">charset</span><span class="hl-code">=</span><span class="hl-quotes">"</span><span class="hl-string">utf-8</span><span class="hl-quotes">"</span><span class="hl-brackets">></span> <head> <link rel="stylesheet" href="style.css" /> <title>My amazing blog</title> </head> <body> <div id="sidebar"> <ul> <li><a href="/">Home</a></li> <li><a href="/blog/">Blog</a></li> </ul> </div> <div id="content"> <h2>Entry one</h2> <p>This is my first entry.</p> <h2>Entry two</h2> <p>This is my second entry.</p> </div> </body> </html> |
请注意,子模版并没有定义sidebar block,所以系统使用了父模版中的值。父模版的{% block %}标签中的内容总是被用作备选内容(fallback)。
您可以根据需要使用多级继承,使用继承的一个常用方式是类似下面的三级结构:
- 创建一个base.html模版来控制您整个站点的主要视觉和体验。
- 为您的站点的每一个“分支”创建一个base_SECTIONNAME.html模版。例如:base_news.html, base_sports.html。这些模版都继承自base.html ,并且包含了每部分特有的样式和设计。
- 为每一种页面类型创建独立的模版,例如新闻内容或者博客文章,这些模版继承对应分支的模版。
这种方式使代码得到最大程度的复用,并且使得添加内容到共享的内容区域更加简单,例如分支范围内的导航。
这里是使用继承的一些提示:
如果你在模版中使用{% extends %}标签,它必须是模版中的第一个标签。其他的任何情况下,模版继承都将无法工作。
在base模版中设置越多的{% block %}标签越好。请记住,子模版不必定义全部父模版中的blocks,所以,你可以在大多数blocks中填充合理的默认内容,然后,只定义你需要的那一个,多一点钩子总比少一点好。
如果你发现你自己在大量的模版中复制内容,那可能意味着你应该把内容移动到父模版中的一个{% block %}中。
如果需要获取父模板中的block的内容,可以使用{{ block.super }}变量。如果你想要在父block中新增内容而不是完全覆盖它,它将非常有用。使用{{ block.super }}插入的数据不会被自动转义(参见下一节),因为父模板中的内容已经被转义。
为了更好的可读性,你也可以给你的{% endblock %}标签一个名字 。例如:
1 2 3 |
{% block content %} ... {% endblock content %} |
在大型模版中,这个方法帮你清楚的看到哪一个{% block %}标签被关闭了。
最后,请注意不能在一个模版中定义多个相同名字的block标签。这个限制的存在是因为block标签的作用是“双向”的。这个意思是,block标签不仅提供了一个坑去填,它定义向父模版的坑中所填的内容。如果在一个模版中有两个名字一样的block标签,模版的父模版将不知道使用哪个block的内容。