从 Jinja 2.0 版本开始学习,之前的版本里有不少编译错误,测试用例也不全,可以先看比较完整的 2.0 版本, 弄清楚主要思路,再看之前的提交过程。
Jinja 2.0
Jinja2 是一个 Python 实现的模板引擎,模板引擎的作用就是把一个 HTML 模板,以及一些环境变量,生成 HTML 页面。模板引擎的使用, 可以使我们在 Web 开发中,将不变的部分(HTML 模板) 与变化的部分(环境变量)分开。HTML 模板中不仅支持对变量值进行替换,还支持 if 语句,for 语句等。
Jinja2 支持的特性有:
各种不同的模板引擎支持的特性对比,可以参考 Comparison of web template engines
__init__.py
: 主模块说明,内部功能导出首先是模样引擎如何解决核心问题,然后是如何支持核心特性
核心问题是,给出静态的 HTML 模板,以及变量值,生成最终返回给客户端的 HTML 文件。
html 模板
Hello {{ user }}
变量
user = 'jack'
最终html文件
Hello jack
如何通过 Jinja2 实现上面的效果
from jinja2 import Environment
print Template("""\
Hello {{ user }}
""").render(user="jack")
通过上面的例子,基本上可以明白模板引擎是在做什么,下面我们看看具体是怎么完成这一功能的。
Jinja 会把 html 模板的代码编译成一段 Python 代码
from __future__ import division
from jinja2.runtime import LoopContext, Context, TemplateReference, Macro, Markup, TemplateRuntimeError, missing, concat, escape, markup_join, unicode_join
name = None
def root(context, environment=environment):
l_user = context.resolve('user')
if 0: yield None
yield u'\n Hello %s\n' % (
l_user,
)
blocks = {}
debug_info = '1=8&2=9'
然后调用这段 Python 代码中的 root
函数,其中 context
变量中存储着变量的值,即上面的例子中 user="jack"
部分。
从上面的过程我们也可以看出,问题的关键就是怎么把那段 html 模板的代码转换为 Python 代码。JinJa 使用 environment.Environment.compile 函数,将 html 代码编译成 Python 代码, 其中当然涉及编译原理的词法分析, 语法分析。我们从最简单的例子开始,逐步体会一下 JinJa 如何将包含各种语法特性的 html 模板转化成 python 代码。
html 模板源代码会依次经过 environment, parser, lexer 模块,最终的词法分析在 lexer.Lexer.tokeniter 函数中完成,它的功能就是将 html 模板源代码转化成 token 流,token 有不同的类型。
最简单的只包含一个变量的模板开始。
<html>
Hello {{ user }}
html>
这段 html 模板构成的 token 流即为:
('\n Hello ', 'data'),
('{{', 'variable_begin'),
(' ', 'whitespace'),
('user', 'name'),
(' ', 'whitespace'),
('}}', 'variable_end'),
('\n', 'data')
每个 token 都由 (值,类型) 构成。
而 parser 模块会使用 lexer 模块得到的 token 流,进行语法分析,生成抽象语法树(AST),并返回。语法分析部分最终在 parser.Parser.subparse 函数中完成。语法分析的最终结果得到了:
[Output(nodes=[
TemplateData(data=u'\n Hello '),
Name(name='user', ctx='load'),
TemplateData(data=u'\n')
]
)
]
然后environment.Environment.compile 函数再使用 compiler.generate 函数将 AST 转化为 Python 代码。转化为 Python 代码的最终函数为 compiler.CodeGenerator.visit_Template 函数。生成的 Python 代码即为:
from __future__ import division
from jinja2.runtime import LoopContext, Context, TemplateReference, Macro, Markup, TemplateRuntimeError, missing, concat, escape, markup_join, unicode_join
name = None
def root(context, environment=environment):
l_user = context.resolve('user')
if 0: yield None
yield u'\n Hello %s\n' % (
l_user,
)
blocks = {}
debug_info = '1=8&2=9'
前面几行是共用的,直接输出即可。
下面这行
l_user = context.resolve('user')
通过 compiler.CodeGenerator.pull_locals 函数生成,如果发现 nodes 中有未定义的变量,就会使用 context 来解析,context 中就包含有 Template.render(user='jack') 中的 {'user': 'jack'} 信息。
后面使用 yield
的那几行,在 compiler.CodeGenerator.visit_Output
中生成。它会依次访问 nodes 中的结点,生成相应的 python 代码。nodes 就是刚才语法分析生成的Output 中的 nodes:
[Output(nodes=[
TemplateData(data=u'\n Hello '),
Name(name='user', ctx='load'),
TemplateData(data=u'\n')
]
)
]
TemplateData 类型的数据会直接进行拼接,Name 类型的会转化成类似 ' %s ' % (luser)
的形式。
上面的例子只包含变量,下面我们看一下添加语法特性后的例子。
这个例子中包含了变量及 if 语句。
完整示例:
from jinja2 import Template
print Template("""\
{% if foo %}
is foo
{% else %}
bar
{% endif %}
""").render(foo = True)
运行这个 python 程序,会输出:
is foo
在这个例子中, html 模板是:
<html>
{% if foo %}
is foo
{% else %}
bar
{% endif %}
html>
生成的 python 代码是:
from __future__ import division
from jinja2.runtime import LoopContext, Context, TemplateReference, Macro, Markup, TemplateRuntimeError, missing, concat, escape, markup_join, unicode_join
name = None
def root(context, environment=environment):
l_foo = context.resolve('foo')
if 0: yield None
yield u'\n '
if l_foo:
if 0: yield None
yield u'\n is foo\n '
else:
if 0: yield None
yield u'\n bar\n '
yield u'\n'
blocks = {}
debug_info = '1=8&2=9&4=14&6=15'
我们可以清楚的看出,模板中的 if 语句就是被转化成了 python 中的 if 语句。对 html 的词法分析得到的 token 流为:
('\n ', 'data'),
('{%', 'block_begin'),
(' ', 'whitespace'),
('if', 'name'),
(' ', 'whitespace'),
('foo', 'name'),
(' ', 'whitespace'),
('%}', 'block_end'),
('\n is foo\n ', 'data'),
...
注意词法分析和语法分析并非独立进行,而是边语法分析,边词法分析,语法分析分析需要 token 时,就从 token 流中获取一个,如何获取是词法分析的事,获取之后用来做什么则由语法分析决定。语法分析最终得到:
[
Output(nodes=[TemplateData(data=u'\n ')]),
If(test=Name(name='foo', ctx='load'),
body= [Output(nodes=[TemplateData(data=u'\n is foo\n ')])],
else_=[Output(nodes=[TemplateData(data=u'\n bar\n ')])]
),
Output(nodes=[TemplateData(data=u'\n')])
]
从这里,应该可以看出抽象语法树中“树”的影子了。生成 AST 后,就是如何将这个 AST 转换成 Python 代码了。
我们不再继续展开,但是已经足够明白 JinJa 的核心部分在做什么: