Template加載機制
一般來說,你在你的文件系統中存入模板,但你也可以使用自定義的template加載器去從其它地方
加載你的模板。
Django有兩種方式去加載你的模板:
1. django.template.loader.get_template(template_name):get_template通過模板名參數,返回
一個模板對象,如果模板不存在,報錯TemplateDoesNotExist
2. django.template.loader.select_template(template_name_list):參數為模板名的列表,返回第一個
存在的模板,如果列表中所有模板都不存在,就報錯TemplateDoesNotExist
以上函數都默認在settings.py中TEMPLATE_DIRS屬性下添加的路徑下查找,不過,在內部機制上,這些函數
也可以指定不同的加載器來完成這些任務。
加載器的設置在settings.py中的TEMPLATE_LOADERS屬性中,它是一個字符串元組類型,每一個字符串代表
一個loader類型。有些被默認開啟,有些是默認關閉的。
可以一起使用,直到找到模板為止。

1. django.template.loaders.filesystem.Loader:默認開啟,從TEMPLATE_DIRS路徑中加載模板
2. django.template.loaders.app_directories.Loader:默認開啟,這個裝載器會在每一個INSTALLED_APPS
注冊的app目錄下尋找templates子目錄,如果有的話,就會在子目錄中加載模板。這樣就可以把模板和
app放在一起,方便重用。這個裝載器會有一些優化,在第一次導入的時候,會緩存包含templates子目錄的app
包的列表。
優化的源碼如下:

3. django.template.loaders.eggs.Loader:默認關閉,從egg文件中加載模板,egg文件類似jar包,python
中打包發布代碼的一種方式。和app_directories Loader類似也是從app子目錄template中加載egg文件。
擴展你的模板系統
一般是擴展模板的tag和filter兩個功能。可以用來創建你自己的tag和filter功能庫。
創建模板庫
分為兩步:
1. 首先決定由模板庫在哪一個
注冊的app下放置,你可以放在一個已有的app目錄下,也可以新建一個
專門管理模板庫的app,比如python manage.py startapp myTemplateLibrary。推薦后者,
因為可以方便將來的重用。
2. 在app目錄下創建templatetags子目錄,並在里面創建兩個文件,__init__.py,用來聲明這是一個
包,另一個是你的tag/filter定義文件。比如myNewLibrary.py,那么在模板文件中可以這樣使用:
{% load myNewLibrary %}
{% load %}只允許導入注冊app目錄下的模板庫。這樣做是為了保證你的模板庫可以不被其它Django
程序使用。
實現自定義過濾器
1. 創建register變量
在你的模塊文件中,你必須首先創建一個全局register變量,它是用來注冊你自定義標簽和過濾器的,
你需要在你的python文件的開始處,插入幾下代碼:
from django
import template
register = template.Library()
register = template.Library()
2. 定義過濾器函數
自定義的過濾器就是一個帶1,2個參數的python函數,一個參數放變量值,一個用來放選項值。
比如{{ var|remove:"bar" }}, var是變量值,"bar"是選項值,remove過濾器可以定義為:
def remove(var, arg)
:
#移除字符串中var的arg字串
return var.replace(arg, '')
#移除字符串中var的arg字串
return var.replace(arg, '')
過濾器函數應該總是返回一些信息,即使出錯,也不應該拋出異常,可以返回默認值或者空字符串。
不帶參數的過濾器也很常見:
def lower(value)
:
"Converts a string into all lowercase"
return value.lower()
"Converts a string into all lowercase"
return value.lower()
3. 注冊過濾器函數
#第一個參數是在模板中使用的過濾器的名字
#第二個就是你的過濾器函數引用名
register. filter( 'remove', remove)
register. filter( 'lower', lower)
#第二個就是你的過濾器函數引用名
register. filter( 'remove', remove)
register. filter( 'lower', lower)
python2.4以上版本,可以使用裝飾符(decorator)功能
@register.
filter(name
=
'remove')
def remove(value, arg) :
return value.replace(arg, '')
@register. filter
def lower(value) :
return value.lower()
def remove(value, arg) :
return value.replace(arg, '')
@register. filter
def lower(value) :
return value.lower()
如果裝飾符不加name,則默認使用函數名來當作使用名。
下面是完整代碼:
from django
import template
register = template.Library()
@register. filter(name = 'remove')
def remove(value, arg) :
return value.replace(arg, '')
@register. filter
def lower(value) :
return value.lower()
register = template.Library()
@register. filter(name = 'remove')
def remove(value, arg) :
return value.replace(arg, '')
@register. filter
def lower(value) :
return value.lower()
實現自定義tag
過程比實現過濾器要復雜,首先回顧一下模板系統的工作流程:
1. 編譯生成模板對象
2. 模板對象使用上下文對象,渲染生成HTML內容
如果你要實現自己的tag,就需要告訴Django怎樣對你的tag進行上面的兩個步驟。
了解模板編譯過程
當Django編譯一個模板時,它把原始的模板文件中的內容變成一個個節點,每一個節點
是django.template.Node的實例,節點都有一個render()函數。因此,一個編譯過的
模板對象可以看成是一個結點對象的列表。例如,模板文件內容:
Hello, {{ person.
name }}.
{% ifequal name.birthday today %}
Happy birthday!
{% else %}
Be sure to come back on your birthday
for a splendid surprise message.
{% endifequal %}
{% ifequal name.birthday today %}
Happy birthday!
{% else %}
Be sure to come back on your birthday
for a splendid surprise message.
{% endifequal %}
被編譯后的Node列表:
- Text node: "Hello, "
- Variable node: person.name
- Text node: ".\n\n"
- IfEqual node: name.birthday and today
當你調用模板對象的render()方法時,它會去調用Node列表上的每一個Node的render方法。最
后輸出的結果就是所有render方法的輸出結果的合並。所以要創建你自己的tag,需要實現你自己的
Node類,實現你自己的render方法。
創建tag實戰
下面我們來實現一個tag,調用方法為:
{% current_time "%Y-%m-%D %I:%M %p" %}
功能是按照給定格式,顯示當前時間,這個格式字符串和time.strftime()中定義的格式一樣
這里是為了演示一下,格式內容可以參考
http://docs.python.org/library/time.html#l2h-1941,
這個標簽也支持不需要參數的默認顯示。
1. 定義Node節點類,實現render方法
import datetime
from django import template
#這一句還是要的
register = template.Library()
class CurrentTimeNode(template.Node) :
def __init__( self, format_string) :
self.format_string = str(format_string)
def render( self, context) :
now = datetime.datetime.now()
#返回的是格式化后的時間表示字符串
return now.strftime( self.format_string)
from django import template
#這一句還是要的
register = template.Library()
class CurrentTimeNode(template.Node) :
def __init__( self, format_string) :
self.format_string = str(format_string)
def render( self, context) :
now = datetime.datetime.now()
#返回的是格式化后的時間表示字符串
return now.strftime( self.format_string)
render函數一定返回的是字符串,即使是空字符串
2. 創建Compilation函數
這個函數主要用於獲取模板中的參數,並創建相應的Node類對象
def do_current_time(parser,
token)
:
try :
tag_name, format_string = token.split_contents()
except ValueError :
msg = '%r tag requires a single argument' % token.split_contents()[ 0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[ 1 : - 1])
每一個tag的編譯函數都需要兩個參數parser和token:
try :
tag_name, format_string = token.split_contents()
except ValueError :
msg = '%r tag requires a single argument' % token.split_contents()[ 0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[ 1 : - 1])
parser是模板分析對象
token是被parser分析后的內容,可以直接使用
token.contents 是tag的內容,這里token的值是'current_time "%Y-%m-%d %I:%M %p"'
token.split_contents 按空格分割字符串,返回tuple,但保留引號之單位的內容,這里得到
('current_time', '%Y-%m-%d %I:%M %p')
和實現filter不一樣,如果tag運行出錯,一定要拋出TemplateSyntaxError,返回一些有用的信息。
不要硬編碼你的tag名,使用token.split_contents()[0]可以得到它。
編譯函數總是返回一個Node子類實例,返回其它類型會報錯。
3. 注冊tag
register.tag('current_time', do_current_time)
和注冊filter類似,兩個參數,一個是使用名,一個是對應的函數引用名
python2.4版本以上,也可以使用裝飾符功能
@register.tag(name
=
"current_time")
def do_current_time(parser, token) :
# ...
@register.tag
def shout(parser, token) :
# ...
不用名字,表示默認使用函數名
def do_current_time(parser, token) :
# ...
@register.tag
def shout(parser, token) :
# ...
完整代碼為:
from django
import template
import datetime
register = template.Library()
@register. filter(name = 'remove')
def remove(value, arg) :
return value.replace(arg, '')
@register. filter
def lower(value) :
return value.lower()
class CurrentTimeNode(template.Node) :
def __init__( self, format_string) :
self.format_string = str(format_string)
def render( self, context) :
now = datetime.datetime.now()
return now.strftime( self.format_string)
def do_current_time(parser, token) :
try :
tag_name, format_string = token.split_contents()
except ValueError :
msg = '%r tag requires a single argument' % token.split_contents()[ 0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[ 1 : - 1])
register.tag( 'current_time', do_current_time)
import datetime
register = template.Library()
@register. filter(name = 'remove')
def remove(value, arg) :
return value.replace(arg, '')
@register. filter
def lower(value) :
return value.lower()
class CurrentTimeNode(template.Node) :
def __init__( self, format_string) :
self.format_string = str(format_string)
def render( self, context) :
now = datetime.datetime.now()
return now.strftime( self.format_string)
def do_current_time(parser, token) :
try :
tag_name, format_string = token.split_contents()
except ValueError :
msg = '%r tag requires a single argument' % token.split_contents()[ 0]
raise template.TemplateSyntaxError(msg)
return CurrentTimeNode(format_string[ 1 : - 1])
register.tag( 'current_time', do_current_time)
4. 運行
在模板文件中添加:

訪問頁面:

復雜的實現自定義tag的其他幾種方法
1. 在Node類的render函數中設置context
def render(
self, context)
:
now = datetime.datetime.now()
#設置context對象的值
context[ 'current_time'] = now.strftime( self.format_string)
# render函數一定要返回字符串,即使是空串
return ''
now = datetime.datetime.now()
#設置context對象的值
context[ 'current_time'] = now.strftime( self.format_string)
# render函數一定要返回字符串,即使是空串
return ''
這樣調用的時候,就是如下用法:
{% current_time
"%Y-%M-%d %I:%M %p" %}
< p>The time is {{ current_time }}.</ p>
< p>The time is {{ current_time }}.</ p>
但這樣做一個不好的地方就是,current_time變量名是硬編碼,可能會覆蓋相同名字的值。
重新設計一個tag的使用格式,如:
{% get_current_time
"%Y-%M-%d %I:%M %p" as my_current_time %}
< p>The current time is {{ my_current_time }}.</ p>
< 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 = str(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) :
#使用正規表達式來處理token
try :
# 使用string.split(sep[, maxsplit]),1代表最大分割數,也就是
# 分割后會產生maxsplit+1個元素
# 這里分割后的結果為(get_current_time, '"%Y-%M-%d %I:%M %p" as my_current_time')
tag_name, arg = token.contents.split( None, 1)
except ValueError :
msg = '%r tag requires arguments' % token.contents.split_contents()[ 0]
raise template.TemplateSyntaxError(msg)
#使用()代表正則組,匹配as兩邊的字符串
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)
# [1:-1]去除格式兩邊的引號
return CurrentTimeNode3( fmt[ 1 : - 1], var_name)
register.tag( 'get_current_time', do_current_time)
class CurrentTimeNode3(template.Node) :
def __init__( self, format_string, var_name) :
#增加自定義變量名的參數
self.format_string = str(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) :
#使用正規表達式來處理token
try :
# 使用string.split(sep[, maxsplit]),1代表最大分割數,也就是
# 分割后會產生maxsplit+1個元素
# 這里分割后的結果為(get_current_time, '"%Y-%M-%d %I:%M %p" as my_current_time')
tag_name, arg = token.contents.split( None, 1)
except ValueError :
msg = '%r tag requires arguments' % token.contents.split_contents()[ 0]
raise template.TemplateSyntaxError(msg)
#使用()代表正則組,匹配as兩邊的字符串
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)
# [1:-1]去除格式兩邊的引號
return CurrentTimeNode3( fmt[ 1 : - 1], var_name)
register.tag( 'get_current_time', do_current_time)
運行結果:

2. 實現塊作用區域的tag
如{% if %}...{% endif %},需要在你的編譯函數中使用parse.parse()
例如我們想要實現{% comment %}...{% endcomment %},功能是
忽略中tag中間的所有內容。
def do_comment(parser,
token)
:
nodelist = parser.parse(( 'endcomment',))
parser.delete_first_token()
return CommentNode()
class CommentNode(template.Node) :
def render( self, context) :
return ''
nodelist = parser.parse(( 'endcomment',))
parser.delete_first_token()
return CommentNode()
class CommentNode(template.Node) :
def render( self, context) :
return ''
parse.parse()的參數是一個包含多個tag名的元組,返回的是它遇到元組中任何一個
tag名之前的所有Node對象列表,所以這里的nodelist包含{% comment %}和
{% endcomment %}之間的所有node對象,並且不包含它們自身兩個node對象。
parser.delete_first_token():因為執行完parse.parse()之后,{% endcomment %}
tag還在,所以需要顯示調用一次,防止這個tag被處理兩次。
3. 在塊作用tag中保留context內容
代碼如下
{% upper %}
This will appear in uppercase, {{ user_name }}.
{% endupper %}
This will appear in uppercase, {{ user_name }}.
{% endupper %}
這里需要context中的user_name參數,怎么才能在處理tag的時候,不丟失context信息呢?
def do_upper(parser,
token)
:
nodelist = parser.parse(( 'endupper',))
parser.delete_first_token()
return UpperNode(nodelist)
class UpperNode(template.Node) :
def __init__( self, nodelist) :
self.nodelist = nodelist
def render( self, context) :
output = self.nodelist.render(context)
return output.upper()
nodelist = parser.parse(( 'endupper',))
parser.delete_first_token()
return UpperNode(nodelist)
class UpperNode(template.Node) :
def __init__( self, nodelist) :
self.nodelist = nodelist
def render( self, context) :
output = self.nodelist.render(context)
return output.upper()
只需要保留下nodelist,然后調用self.nodelist.render(contest),就可以間接調用每一個Node
的render函數。
有更多的例子,可以查看Django源代碼,位置為:
D:\Python27\Lib\site-packages\django\template\defaulttags.py
4. 快速創建簡單tag的方法
簡單tag的定義,只帶一個參數,返回經過處理的字符串,像之前的current_time標簽一樣。
Django提供了一種simple_tag方法來快速創建類似這樣的tag。
def current_time(format_string)
:
try :
return datetime.datetime.now().strftime( str(format_string))
except UnicodeEncodeError :
return ''
register.simple_tag(current_time)
try :
return datetime.datetime.now().strftime( str(format_string))
except UnicodeEncodeError :
return ''
register.simple_tag(current_time)
simple_tag參數為一個函數引用,會把它包裝進render函數中,然后再進行注冊。也不用定義
Node子類了。
python2.4以上,可以使用裝飾符
@register.simple_tag
def current_time( token) :
# ...
def current_time( token) :
# ...
5. 創建Inclusion Tag
另外一種比較普遍的tag類型是只是渲染其它模塊顯示下內容,這樣的類型叫做Inclusion Tag。
例如,實現以下tag:
{% books_for_author author %}
渲染結果為:
<
ul>
< li>The Cat In The Hat</ li>
< li>Hop On Pop</ li>
< li>Green Eggs And Ham</ li>
</ ul>
< li>The Cat In The Hat</ li>
< li>Hop On Pop</ li>
< li>Green Eggs And Ham</ li>
</ ul>
列出某個作者所有的書。
- 定義函數
def books_for_author(author)
:
books = Book.objects. filter(authors__id =author. id)
return { 'books' : books}
books = Book.objects. filter(authors__id =author. id)
return { 'books' : books}
- 創建另一個模板文件book_snippet.html
<
ul>
{% for book in books %}
< li>{{ book. title }}</ li>
{% endfor %}
</ ul>
{% for book in books %}
< li>{{ book. title }}</ li>
{% endfor %}
</ ul>
- 注冊tag
register.inclusion_tag('book_snippet.html')(books_for_author)
有些你的模板可以使用父模板的context內容,Django提供一個takes_context參數來實現,
使用之后,tag不能再帶參數,
@register.inclusion_tag(
'link.html', takes_context
=
True)
def jump_link(context) :
return {
'link' : context[ 'home_link'],
'title' : context[ 'home_title'],
}
模板文件link.html為
def jump_link(context) :
return {
'link' : context[ 'home_link'],
'title' : context[ 'home_title'],
}
Jump directly to <a href="{{ link }}">{{ title }}</a>.
使用方法:
{% jump_link %}
創建自定義模板加載類
可以自定其它的加載行為,比如從數據庫中加載,從svn中加載,從zip中加載等。
需要實現一個接口load_template_source(template_name, template_dirs=None):
template_name就是類似'link.html'這樣的模板名稱
template_dirs是一個可選的路徑列表,為空就使用TEMPLATE_DIRS屬性定義的路徑。
如果一個加載器加載模板成功,它將返回一個元組(template_source, template_path)。
template_source:模板文件的內容字符中,會用於被編譯
template_path:模板文件的路徑
如果加載失敗,報錯django.template.TemplateDoesNotExist
每一個加載函數都需要有一個is_usable的函數屬性,對,
是函數的屬性,因為在python中,
函數也是個對象。這個屬性告訴模板引擎在當前的python環境下這個加載器是否可用。
例如,之前的eggs加載器是默認關閉的,is_usable=False,因為需要pkg_resources模塊
中的從egg中讀取信息的功能。不一定每個用戶會安裝,如果安裝了,就可以設置為True,開啟
功能。
下面實現一個從zip文件中加載模板的自定義加載器,它使用TEMPLATE_ZIP_FILES作為搜索路徑,來
代替系統的TEMPLATE_DIRS,路徑上都是zip文件名。
from django.conf
import settings
from django.template import TemplateDoesNotExist
import zipfile
def load_template_source(template_name, template_dirs = None) :
"Template loader that loads templates from a ZIP file."
#從settings.py配置文件中讀取屬性TEMPLATE_ZIP_FILES的值,默認返回空列表
template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])
# Try each ZIP file in TEMPLATE_ZIP_FILES.
for fname in template_zipfiles :
try :
z = zipfile.ZipFile(fname)
source = z.read(template_name)
except ( IOError, KeyError) :
continue
z.close()
# 找到一個可用的文件就返回
template_path = "%s:%s" % (fname, template_name)
return (source, template_path)
# 如果一個zip文件沒找到,報錯
raise TemplateDoesNotExist(template_name)
# 設置為可用
load_template_source.is_usable = True
from django.template import TemplateDoesNotExist
import zipfile
def load_template_source(template_name, template_dirs = None) :
"Template loader that loads templates from a ZIP file."
#從settings.py配置文件中讀取屬性TEMPLATE_ZIP_FILES的值,默認返回空列表
template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])
# Try each ZIP file in TEMPLATE_ZIP_FILES.
for fname in template_zipfiles :
try :
z = zipfile.ZipFile(fname)
source = z.read(template_name)
except ( IOError, KeyError) :
continue
z.close()
# 找到一個可用的文件就返回
template_path = "%s:%s" % (fname, template_name)
return (source, template_path)
# 如果一個zip文件沒找到,報錯
raise TemplateDoesNotExist(template_name)
# 設置為可用
load_template_source.is_usable = True
保存為zip_loader.py,放在app目錄下,剩下我們需要做的是,在TEMPLATE_LOADERS屬性
中注冊你的加載器:
TEMPLATE_LOADERS
= (
'books.zip_loader.load_template_source',
)
'books.zip_loader.load_template_source',
)