最近项目中有个关于 Django 模板根据配置动态加载不同 css 文件的需求,因为对 Django 模板渲染的过程不够熟悉导致很简单的一个功能耽误了特别长的时间。趁着周末阅读了相关的文档和源码,对整个 Django 模板的渲染过程有了一个大致的了解,记录与此作为备忘,参考资料也一并记录下来。
一. Django 模板的渲染过程详解
首先是示例代码,看下面这段 HTML 代码 welcome.html:
<!DOCTYPE html>
{% load g_theme %}
{% load custom_tag %}
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
# 加载 context 中的 first 变量
{{ first }} <br>
# 通过自定义的 upper 过滤器实现字母的大小写转换
{{ "abc" | upper }} <br>
{{ "CDE" | lower }} <br>
# 通过自定义标签输出相关信息
{% user_info zouyingjie 24 %} <br>
</body>
</html>
下面是 对应的 View 的代码,在 context 中加入了两个变量: first 和 second 。(忽略蹩脚的命名 o(╯□╰)o)
class WelcomeView(TemplateView):
template_name = "welcome.html"
def get_context_data(self, **kwargs):
return {
"first": "Ashe",
}
渲染后的界面如下, 可以看到 first 变量的值,user 信息以及大小写的转换都成功了。
上面就是主要代码和效果,那么整个模板是怎么渲染出来的呢?下面就跟着源码一步步的来分析吧。
首先,这里对 HTML 模板的请求会在 WelcomeView 进行接收,就从这里开始看吧
WelcomeView 继承自 TemplateView, 下面是其代码:
class TemplateView(TemplateResponseMixin, ContextMixin, View):
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
class TemplateResponseMixin(object):
"""
A mixin that can be used to render a template.
"""
template_name = None
template_engine = None
response_class = TemplateResponse
content_type = None
def render_to_response(self, context, **response_kwargs):
response_kwargs.setdefault('content_type', self.content_type)
return self.response_class(
request=self.request,
template=self.get_template_names(),
context=context,
using=self.template_engine,
**response_kwargs
)
def get_template_names(self):
if self.template_name is None:
raise ImproperlyConfigured(
"TemplateResponseMixin requires either a definition of "
"'template_name' or an implementation of 'get_template_names()'")
else:
return [self.template_name]
可以看到, TemplateView 继承了 TemplateResponseMixin, 其 render_to_response 就是在这里定义的,最终返回了 TemplateResponse 的实例。对参数作简要说明:
- template: 要渲染的模板,这里就是我们的 welcome.html, 只不过添加到了一个列表中
- context: 我们创建的 context 数据。 context 数据的添加主要有两种方式,一是在具体的 View 中重写 get_context_data() 方法进行定制化的添加;第二种是自定义 context_processors 进行全局性的添加。在文后的参考资料中有详细的讲解,可以参阅。
接下来就要看返回的 TemplateResponse 的实例内容了,其继承了 SimpleTemplateResponse 类,关于模板的渲染主要在这里进行,最终会调用其 render 方法进行渲染,这里具体的调用过程又是关于网络请求的一套流程了,这里不作赘述,后面会专门写文章来分析Django 完成整个请求过程的源码。
下面是 TemplateResponse 部分代码, 略去了部分无需关心的代码,首先是 render 方法,其调用了 render_content 属性。
def render(self):
retval = self
if not self._is_rendered:
# 调用 rendered_content 属性
self.content = self.rendered_content
for post_callback in self._post_render_callbacks:
newretval = post_callback(retval)
if newretval is not None:
retval = newretval
return retval
@property
def rendered_content(self):
# 1.将 html 模板解析一个 Template 对象
template = self.resolve_template(self.template_name)
# 2.得到 context 数据
context = self.resolve_context(self.context_data)
# 3.根据 context 数据对 Template 进行渲染
content = template.render(context, self._request)
return content
def resolve_template(self, template):
"Accepts a template object, path-to-template or list of paths"
if isinstance(template, (list, tuple)):
return select_template(template, using=self.using)
elif isinstance(template, six.string_types):
return get_template(template, using=self.using)
else:
return template
def resolve_context(self, context):
return context
现在我们知道了,整个过程主要分为两步: * 获取 Template 对象 和 context 数据 以及 根据 context 对 Template 对象进行渲染*。下面就对这两个过程进行分析。
1. 解析 html 文件为 Template 对象
在上面 TemplateResponse 的初始化代码中,我们知道其将我们声明的 template_name 添加进了一个列表中返回,因此在 resolve_template 方法中调用的是select_template 方法。
Django有两种方法加载模板
django.template.loader.get_template(template_name) : get_template 根据给定的模板名称返回一个已编译的模板(一个 Template 对象)。 如果模板不存在,就触发 TemplateDoesNotExist 的异常。
django.template.loader.select_template(template_name_list) : select_template 很像 get_template ,不过它是以模板名称的列表作为参数的。 它会返回列表中存在的第一个模板。 如果模板都不存在,将会触发TemplateDoesNotExist异常。
select_template 后方法的调用过程是:select_template 方法调用 DjangoTemplates 的 get_template 方法,该方法又调用 Engine 实例 find_template 方法,然后调用 Loader 的 loader.get_template 方法,最终在该方法中读取 html 文件的内容,然后初始化为 Template 对象。其代码如下:
# Loder 的 get_template 方法
def get_template(self, template_name, template_dirs=None, skip=None):
# origin 代表一个 html 文件
for origin in self.get_template_sources(*args):
if skip is not None and origin in skip:
tried.append((origin, 'Skipped'))
continue
try:
# 通过正常的文件 IO 读到 html 文件的内容
contents = self.get_contents(origin)
except TemplateDoesNotExist:
tried.append((origin, 'Source does not exist'))
continue
else:
# 传递 content 进行初始化
return Template(
contents, origin, origin.template_name, self.engine,
)
OK ,现在已经读取到了 HTML 的文本内容,下面就是解析为 Template 了。Template 代码方法如下:
class Template(object):
def __init__(self, template_string, origin=None, name=None, engine=None):
try:
template_string = force_text(template_string)
except UnicodeDecodeError:
raise TemplateEncodingError("Templates can only be constructed "
"from unicode or UTF-8 strings.")
if engine is None:
from .engine import Engine
engine = Engine.get_default()
if origin is None:
origin = Origin(UNKNOWN_SOURCE)
self.name = name
self.origin = origin
self.engine = engine
# source 值就是读取到的 html 文件内容
self.source = template_string
# 解析 html 文本内容为 nodelist
self.nodelist = self.compile_nodelist()
def compile_nodelist(self):
if self.engine.debug:
lexer = DebugLexer(self.source)
else:
lexer = Lexer(self.source)
tokens = lexer.tokenize()
parser = Parser(
tokens, self.engine.template_libraries, self.engine.template_builtins,
self.origin,
)
try:
return parser.parse()
except Exception as e:
if self.engine.debug:
e.template_debug = self.get_exception_info(e, e.token)
raise
主要看 compile_nodelist 代码,细节比较多,这里简要介绍下流程, 对于细节不作深究:
- Lexer 负责将 HTML 文件按照文件节点进行分割,生成一个 tokens 列表。其实就是根据正则表达式对整个文件内容进行切分,正则和解析代码如下,可以看到我们模板中使用的各个符号都有匹配。
- parser 根据 tokens 和 context 数据对模板进行解析
# template syntax constants
FILTER_SEPARATOR = '|'
FILTER_ARGUMENT_SEPARATOR = ':'
VARIABLE_ATTRIBUTE_SEPARATOR = '.'
BLOCK_TAG_START = '{%'
BLOCK_TAG_END = '%}'
VARIABLE_TAG_START = '{{'
VARIABLE_TAG_END = '}}'
COMMENT_TAG_START = '{#'
COMMENT_TAG_END = '#}'
TRANSLATOR_COMMENT_MARK = 'Translators'
SINGLE_BRACE_START = '{'
SINGLE_BRACE_END = '}'
# what to report as the origin for templates that come from non-loader sources
# (e.g. strings)
UNKNOWN_SOURCE = '<unknown source>'
# match a variable or block tag and capture the entire tag, including start/end
# delimiters
tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' %
(re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END))))
def tokenize(self):
"""
Split a template string into tokens and annotates each token with its
start and end position in the source. This is slower than the default
lexer so we only use it when debug is True.
"""
lineno = 1
result = []
upto = 0
# 根据 tag_re 进行正则匹配,
for match in tag_re.finditer(self.template_string):
start, end = match.span()
if start > upto:
token_string = self.template_string[upto:start]
result.append(self.create_token(token_string, (upto, start), lineno, in_tag=False))
lineno += token_string.count('\n')
upto = start
token_string = self.template_string[start:end]
result.append(self.create_token(token_string, (start, end), lineno, in_tag=True))
lineno += token_string.count('\n')
upto = end
last_bit = self.template_string[upto:]
if last_bit:
result.append(self.create_token(last_bit, (upto, upto + len(last_bit)), lineno, in_tag=False))
return result
下面就是 welcome.html 文件解析到的 tokens 列表,
生成 tokens 之后就是通过 Parser 解析为 Node 了,代码细节比较类似于 Java 中解析 XML 文件的方式,就是一个个的 tag 匹配,这里不上代码了,引用一段笔记:
>
1. 用 {% ... %}
語法寫出的元件代表一個 block 的開始或結束(或者一整個 block 本身)。Django 會根據該元件宣告時的定義,收集整個 block 中的資訊,轉為一個 block token。這是唯一一種內部可以有 child tokens 的元件。
2. 用 {# ... #}
寫出的元件會被轉為 comment token,代表註解。它會在 template 變成 HTML 時被捨棄。
3. 用 {{ ... }}
寫出的元件會被轉為 variable token。Django 會根據裡面的 variable name 從 context 中尋找它的值,再檢查後面有沒有接 filters(|
語法),如果有就進行額外的處理,然後輸出。
4. 其他所有輸入都會成為 text token,會直接被輸出。
另外补充的一点是,如果有自定义的 tag, 会执行其编译函数,只要返回的是一个 Node 即可。因此除了给定的通过装饰器的方式自定义 tag 之外,还可以通过自定义 Node 和编译函数的方式完成。
解析完成后得到一个 NodeList 的列表,我们 welcome.html 得到的列表如下, 可以看到有 LoadNode, InclusionNode, TextNode, VariableNode 等类型。
OK。上面就是获取 Template 对象的过程了。细节比较麻烦,不过流程还算是比较清晰。接下来就是 Template 的渲染了。
2. Template 模板的渲染
首先调用的是 Template 的 render 方法,其内部会对所有的 context 数据进行整合,合并为一个 dict 的列表,最终调用的是 NodeList 的 render 方法对各个 Node 进行渲染,其代码如下:
class NodeList(list):
# Set to True the first time a non-TextNode is inserted by
# extend_nodelist().
contains_nontext = False
def render(self, context):
bits = []
for node in self:
if isinstance(node, Node):
bit = node.render_annotated(context)
else:
bit = node
bits.append(force_text(bit))
return mark_safe(''.join(bits))
最终就是循环所有的 Node 对象,不同的 Node 对象有不同的解析方法,每个返回一个文本添加到 bits 列表中,最后在整理为一个完整的文本进行返回。 下面对Node 的解析规则做一个简单的介绍:
- TextNode: 主要是原始的 HTML 界面,这部分原样返回
- LoadNode: 返回 ”。因此在客户端的代码中看不到 load 代码
- VariableNode: 返回变量值
- InclusionNode: 这里就是对 inclusion_tag 的渲染,会再次遍历其引用的 HTML 进行一次渲染,返回引入的 HTML 文档。
上面就是本次用到的几个 Node 的简介,具体代码就不一一呈现了,只要了解了各种元素对应的 Node,需要的时候查看对应的源码即可。
最后可以看到每个 Node 解析后的文本都加入到了 bits 列表中,然后用 join 方法又汇集成了一个完成的文本进行返回。这样我们就得到了想要的 HTML 界面。
以上就是 Django Template 模板的大致渲染过程了,重在流程分析,对于细节没做过多的深入,参考的两篇文档对 Django 模板的使用和原理做了很好的讲解,认真看几遍之后在读下源码对于 Django Template 的使用应该问题不大了。希望对需要的同学有所帮助。