详解Python的Django框架中的模版继承
在实际应用中,你将用Django模板系统来创建整个HTML页面。这就带来一个常见的Web开发问题:在整个网站中,如何减少共用页面区域(比如站点导航)所引起的重复和冗余代码?
解决该问题的传统做法是使用服务器端的includes,你可以在HTML页面中使用该指令将一个网页嵌入到另一个中。事实上,Django通过刚才讲述的{%include%}支持了这种方法。但是用Django解决此类问题的首选方法是使用更加优雅的策略——模板继承。
本质上来说,模板继承就是先构造一个基础框架模板,而后在其子模板中对它所包含站点公用部分和定义块进行重载。
让我们通过修改current_datetime.html文件,为current_datetime创建一个更加完整的模板来体会一下这种做法:
<!DOCTYPEHTMLPUBLIC"-//W3C//DTDHTML4.01//EN"> <htmllang="en"> <head> <title>Thecurrenttime</title> </head> <body> <h1>Myhelpfultimestampsite</h1> <p>Itisnow{{current_date}}.</p> <hr> <p>Thanksforvisitingmysite.</p> </body> </html>
这看起来很棒,但如果我们要为第三章的hours_ahead视图创建另一个模板会发生什么事情呢?
<!DOCTYPEHTMLPUBLIC"-//W3C//DTDHTML4.01//EN"> <htmllang="en"> <head> <title>Futuretime</title> </head> <body> <h1>Myhelpfultimestampsite</h1> <p>In{{hour_offset}}hour(s),itwillbe{{next_time}}.</p> <hr> <p>Thanksforvisitingmysite.</p> </body> </html>
很明显,我们刚才重复了大量的HTML代码。想象一下,如果有一个更典型的网站,它有导航条、样式表,可能还有一些JavaScript代码,事情必将以向每个模板填充各种冗余的HTML而告终。
解决这个问题的服务器端include方案是找出两个模板中的共同部分,将其保存为不同的模板片段,然后在每个模板中进行include。也许你会把模板头部的一些代码保存为header.html文件:
<!DOCTYPEHTMLPUBLIC"-//W3C//DTDHTML4.01//EN"> <htmllang="en"> <head>
你可能会把底部保存到文件footer.html:
<hr> <p>Thanksforvisitingmysite.</p> </body> </html>
对基于include的策略,头部和底部的包含很简单。麻烦的是中间部分。在此范例中,每个页面都有一个<h1>Myhelpfultimestampsite</h1>标题,但是这个标题不能放在header.html中,因为每个页面的<title>是不同的。如果我们将<h1>包含在头部,我们就不得不包含<title>,但这样又不允许在每个页面对它进行定制。何去何从呢?
Django的模板继承系统解决了这些问题。你可以将其视为服务器端include的逆向思维版本。你可以对那些不同的代码段进行定义,而不是共同代码段。
第一步是定义基础模板,该框架之后将由子模板所继承。以下是我们目前所讲述范例的基础模板:
<!DOCTYPEHTMLPUBLIC"-//W3C//DTDHTML4.01//EN"> <htmllang="en"> <head> <title>{%blocktitle%}{%endblock%}</title> </head> <body> <h1>Myhelpfultimestampsite</h1> {%blockcontent%}{%endblock%} {%blockfooter%} <hr> <p>Thanksforvisitingmysite.</p> {%endblock%} </body> </html>
这个叫做base.html的模板定义了一个简单的HTML框架文档,我们将在本站点的所有页面中使用。子模板的作用就是重载、添加或保留那些块的内容。(如果你一直按顺序学习到这里,保存这个文件到你的template目录下,命名为base.html.)
我们使用一个以前已经见过的模板标签:{%block%}。所有的{%block%}标签告诉模板引擎,子模板可以重载这些部分。每个{%block%}标签所要做的是告诉模板引擎,该模板下的这一块内容将有可能被子模板覆盖。
现在我们已经有了一个基本模板,我们可以修改current_datetime.html模板来使用它:
{%extends"base.html"%} {%blocktitle%}Thecurrenttime{%endblock%} {%blockcontent%} <p>Itisnow{{current_date}}.</p> {%endblock%}
再为hours_ahead视图创建一个模板,看起来是这样的:
{%extends"base.html"%} {%blocktitle%}Futuretime{%endblock%} {%blockcontent%} <p>In{{hour_offset}}hour(s),itwillbe{{next_time}}.</p> {%endblock%}
看起来很漂亮是不是?每个模板只包含对自己而言独一无二的代码。无需多余的部分。如果想进行站点级的设计修改,仅需修改base.html,所有其它模板会立即反映出所作修改。
以下是其工作方式。在加载current_datetime.html模板时,模板引擎发现了{%extends%}标签,注意到该模板是一个子模板。模板引擎立即装载其父模板,即本例中的base.html。
此时,模板引擎注意到base.html中的三个{%block%}标签,并用子模板的内容替换这些block。因此,引擎将会使用我们在{blocktitle%}中定义的标题,对{%blockcontent%}也是如此。所以,网页标题一块将由{%blocktitle%}替换,同样地,网页的内容一块将由{%blockcontent%}替换。
注意由于子模板并没有定义footer块,模板系统将使用在父模板中定义的值。父模板{%block%}标签中的内容总是被当作一条退路。
继承并不会影响到模板的上下文。换句话说,任何处在继承树上的模板都可以访问到你传到模板中的每一个模板变量。
你可以根据需要使用任意多的继承次数。使用继承的一种常见方式是下面的三层法:
- 创建base.html模板,在其中定义站点的主要外观感受。这些都是不常修改甚至从不修改的部分。
- 为网站的每个区域创建base_SECTION.html模板(例如,base_photos.html和base_forum.html)。这些模板对base.html进行拓展,并包含区域特定的风格与设计。
- 为每种类型的页面创建独立的模板,例如论坛页面或者图片库。这些模板拓展相应的区域模板。
这个方法可最大限度地重用代码,并使得向公共区域(如区域级的导航)添加内容成为一件轻松的工作。
以下是使用模板继承的一些诀窍:
- 如果在模板中使用{%extends%},必须保证其为模板中的第一个模板标记。否则,模板继承将不起作用。
- 一般来说,基础模板中的{%block%}标签越多越好。记住,子模板不必定义父模板中所有的代码块,因此你可以用合理的缺省值对一些代码块进行填充,然后只对子模板所需的代码块进行(重)定义。俗话说,钩子越多越好。
- 如果发觉自己在多个模板之间拷贝代码,你应该考虑将该代码段放置到父模板的某个{%block%}中。
- 如果你需要访问父模板中的块的内容,使用{{block.super}}这个标签吧,这一个魔法变量将会表现出父模板中的内容。如果只想在上级代码块基础上添加内容,而不是全部重载,该变量就显得非常有用了。
- 不允许在同一个模板中定义多个同名的{%block%}。存在这样的限制是因为block标签的工作方式是双向的。也就是说,block标签不仅挖了一个要填的坑,也定义了在父模板中这个坑所填充的内容。如果模板中出现了两个相同名称的{%block%}标签,父模板将无从得知要使用哪个块的内容。
- {%extends%}对所传入模板名称使用的加载方法和get_template()相同。也就是说,会将模板名称被添加到TEMPLATE_DIRS设置之后。
- 多数情况下,{%extends%}的参数应该是字符串,但是如果直到运行时方能确定父模板名,这个参数也可以是个变量。这使得你能够实现一些很酷的动态功能。