tornado模板引擎原理

前言

老师问小明:已经a=1,

求a+1的值。

小明挠挠头,思考后回答:老师,a+1的结果是2。

以上是一个非常简单的例子,实际上就是一个模板编译过程。

a=1,表示一个命名空间(namespace)中的一项,该命名空间中存在一个名称(name)为a,其为1。在python里面,命名空间可以用数据结构字典来表示。

a+1,实际上是一个非常简单的模板。这个模板里面有且只有一条规则a+1。本博文中所说的模板,实际上更类似一份空白的数学考卷。这份数学考卷中,有很多类似上面老师问小明的题目。

学生解决这份考卷的过程,就是一个模板的编译过程。学生解答每一道题目获得答案,类似于模板每一个节点编译。一个模板是由很多节点组成。这样看来我们解决“模板编译”问题,可以转化成解决各个节点编译问题。反过来说,只要各个节点的编译问题解决了,整个模板的编译也就解决了。我们通常将一个大问题,转化成各个并行不相干的小问题(可以认为:一个大问题等于n个小问题相加),然后各个击破。

不过有时候,会遇到比a+1更加难一点的问题:

老师对小明说:现在要求a+1的值,但现在a是一个未知数,只知道a=b+1,

不过我们知道b=1。

小明思考了一分钟,对老师说:a+1等于3,

我们知道要想解决a+1的值,a的值首先要获得,我们称解决a的值解决a+1问题不可或缺的一个嵌套子问题。只有这个嵌套子问题获得解决,原来的问题才能获得解决的。所幸,老师提供a=b+1,而且知道了b=1,那么小明可以知道a=2,进而知道a+1等于3。这样思考过程,本质上对应于递归算法解决问题的思路。那么我们可以用计算机模拟这种问题的编译。

总结一下数学考试的过程,答一份数学考卷的过程。如果答一份数学考卷交给机器人来做,会用到怎样的算法实现?

伪代码如下:

def solved_big_problem(big_problem):
    small_problem_list = split(big_problem)
    answer = ""
    foreach small_problem in small_problem_list:
        if is_problem_small_enough(small_problem):
            answer+=small_problem.done()
        else:
            answer+=solved_big_problem(big_problem)
    return answer

tornado模板引擎如何划分小问题

tornado template引擎中,将整个静态模板划分成不同类型的节点。

在我认为,template中主要有三种类型的节点(Node)

1. 文本节点(TextNode)

2. 注释节点(CommentNode)

3. 表达式节点(ExpressionNode)

4. 块节点(BlockNode)

文本节点

一个文本节点是一个不会被命名空间数据渲染的字符串,也就是模板中,该字符串是什么,最后编译结束后输出的就是什么。

注释节点

一个注释节点,以特殊标签”{#”开始,以“#}”结束,中间是一个对模板的注释字符串,注释节点在编译结束后,不会产生任何输出。

表达式节点

一个表达式节点,以特殊标签”((“开始,以“}}”结束,中间是一个字符串,这个字符串可能仅仅是一个名称的名字,编译后会被命名空间中该名称对应的值替换掉。也可能是一个字面量。更或者是一个复杂的表达式,在c++语言中,我们说一个表达式可以作为右值,用在等号的右边,赋值给一个变量。表达式的编译结果会被给表达式实际的值所替换。一个表达式中可能包含运算或者函数调用。

块节点

块节点是节点中最复杂的一种。通常以”{%”开始,以“ %}”结束。“{%…%}”中间的第一个单词,称之为指令(operator)。多个块节点将组合成更大的模板单位。我们将看到这些模板单位,会被映射成python语言中的,控制块, 循环体, 异常处理块,语句等等。

指令 用法
apply {% apply function %}…{% end %}
autoescape {% autoescape function %}
block {% block name %}…{% end %}
comment {% comment … %}
extends {% extends filename %}
for {% for var in expr %}…{% end %}
from {% from x import y %}
if {% if condition %}…{% elif condition %}…{% else %}…{% end %}
import {% import module %}
include {% include filename %}
module {% module expr %}
raw {% raw expr %}
set {% set x = y %}
try {% try %}…{% except %}…{% else %}…{% finally %}…{% end %}
while {% while condition %}… {% end %}
break {% break %}
continue {% continue %}

模板解析

模板解析的过程实际上就是找出静态模板字符串中各种节点。

节点类

class _Node(object):
    def each_child(self):
        return ()

    def generate(self, writer):
        raise NotImplementedError()

    def find_named_blocks(self, loader, named_blocks):
        for child in self.each_child():
            child.find_named_blocks(loader, named_blocks)

def each_child(self):

一个节点的话,可能会包含很多子节点,提供这个接口,返回该节点所有子节点。

def generate(self, writer):

将该节点映射成python语言代码

def find_named_blocks(self, loader, named_blocks):

找到节点body中包含的命名块,此接口在处理模板继承时候作用重大。

_Node子类 表示节点类型
_File 整个模板
_ChunkList 多个并列节点的集合,也可作为一个节点的body
_NamedBlock 命名块,对应block指令 ,在模板继承中起作用
_ExtendsBlock 对应extends指令
_IncludeBlock 对应include指令
_ApplyBlock 对应apply指令
_ControlBlock 对应if指令,控制块
_IntermediateControlBlock 对应else,elif,except,finally指令
_Statement 对应import,from,set,break,continue指令
_Expression 对应raw指令
_Module 对应module指令
_Text 对应文本节点

解析算法

tornado模板节点按是否含有子节点来说,其实只分为两类。和”{% end %}”结合才起作用的节点,称之为宿主节点。不必和”{% end %}”结合就能起作用的节点,称之为”直接节点“。另外称”{% end %}”为结束节点

宿主节点不是一个简单的节点,里面可以寄居若干子节点,形成树状层级结构。直接节点可能是一个表达式,或者语句,不含有子节点,这种情况是很容易处理的。

而“宿主块节点”该如何解析?其实类似于我们熟悉的问题-括号匹配。因为一个宿主块节点和“{% end %}”,结合才起作用,类似一对括号,只有左右括号都存在的时候,才是一个完整的括号,起到它应该有的意义。

一般上过数据结构课的人都知道,可以通过数据结构,解决括号匹配问题,其实也可以解决宿主节点解析问题。

如果宿主节点含有子节点,如下模板代码。

{% block content %}
    {% for line in lines %}
        <p>line</p>
    {% end %}
    {% if user=="admin" %}
        <p>welcome admin!</p>
    {% end %}
    <p>just test</p>
{% end %}

我们换另外一种思路,递归思想来考虑这个问题,发现也非常直接。简单伪代码如下(流程不太严谨)。

def parse(template_string):
   child_nodes = []
   while !template_string.end():
       node = get_next_node(template_string)
       if is_end_node(node):
           return child_nodes
       if is_has_child_nodes(node):
           template_string = move(template_string)
           node.child_nodes = parse(template_string)
       else:
           node.child_nodes = []
       child_nodes.append(node)
       template_string = move(template_string)
   return child_nodes  

所有在模板中节点,按照父子关系组织起来,是一个森林的数据结构。解析整个模板的过程,就是构造森林的过程。这棵森林所有子树的叶子节点,可以是文本节点,注释节点,表达式节点和块节点中具有表达式意义或者语句意义的节点。而内部节点是宿主节点,但解析遇到一个结束节点,就知道一个宿主节点的所有子节点都解析完成,是时候return。

tornado中对模板的解析,是在template模块中_parse函数。

模板编译

在web开发,利用模板,目的是通过不同数据渲染相同模板,结果产生针对用户的不同内容页面。整个过程就是一个顶级函数,传入参数两个:一个是模板字符串,另外一个是数据(命名空间),返回的是一个字符串(最终用户在浏览器中看到的内容页面)。

我们在模板字符串中挖的洞,最后需要被实际的数据填补起来。所以关键问题是该如何去填补这些

tornado中的做法非常干脆利落,直接将模板字符串,翻译成一个python函数,然后编译这个python函数,如果web server接受到客户端请求。就直接调用这个编译好的函数,返回的结果就是一个内容页面。

强大工具

所幸,python语言为tornado模板引擎实现,提供了极大便利——内建函数compileexec语句

内建函数compile

python中提供了一个内建函数compile,可以编译一个源码字符串成一个code对象,返回的code对象,能够被exec语句或者eval()函数执行。

exec语句

exec语句支持python代码动态执行。

python2.7中的语法如下:

exec code[ in globals[,locals]]

code可以是一个字符串,一个打开的文件对象或者一个code对象。如果是字符串,会被解析成python语句执行。如果是文件对象,会一直解析到EOF然后执行。如果是code对象,已经不需要解析,可以直接执行。

exec语句也可以看作一个函数(实际上,在python3上,exec就是一个函数),globals(字典结构)和locals(字典结构)都是exec函数的可选参数。如果两者都没有提供,code在当前exec语句的作用域上下文下执行(code中用到全局作用域的对象和局部作用域的对象,并且可以对它们做修改)。

上面提到exec语句执行不提供任何的可选参数,是非常糟糕的,code执行会污染exec语句所在的作用域,并且引发很多难以调试的bug。

globals参数,可以避免这种情况。globals参数,让我们可以自定义code执行的作用域上下文,code执行只能获取globals中提供的变量、函数、类型等等,不过有一种例外—-python中的buildins。在code执行前,exec语句会在globals中,插入一项,key为”_builtins_“。这样code执行可以利用python中所有的buildins(tornado模板中可以利用所用python内建buildins)。

既然code在自定义的作用域上下文执行(相当于在一个沙盒子中),如果我们想在exec语句执行完毕,或者code中定义的类型,变量,函数等等,那该怎么办?

locals参数正是为此而来。

_code = ‘‘‘
def my_output():
    print "hello world"
‘‘‘
def main():
    compiled = compile(_code, "<string>", "exec")
    global_dict = {}
    local_dict = {}
    exec compiled in global_dict, local_dict
    local_dict["my_output"]()
if __name__ == ‘__main__‘:
    main()

但是如果locals参数没有提供,只提供了globals参数。exec语句执行完毕,我们还是可以访问到code中定义的变量,类型,函数。exec语句把这一切都插入到了globals字典中。

我们可以看到,当提供了globals和locals参数,exec中的code执行,如果在一个沙盒子中,可以避免对exec语句所在作用域的污染。

tornado模板引擎如何应用这些工具

从模板节点生成代码

还记得在template模块中的_Node基类提供了一个没有实现的方法–generate。每种模板节点都有自己生成代码的方式,generate方法将在子类中实现。

class _File(_Node):
    def __init__(self, template, body):
        self.template = template
        self.body = body
        self.line = 0

    def generate(self, writer):
        writer.write_line("def _tt_execute():", self.line)
        with writer.indent():
            writer.write_line("_tt_buffer = []", self.line)
            writer.write_line("_tt_append = _tt_buffer.append", self.line)
            self.body.generate(writer)
            writer.write_line("return _tt_utf8(‘‘).join(_tt_buffer)", self.line)

    def each_child(self):
        return (self.body,)

我们先来看看_File类的generate方法。_File实际上对应的是整个模板文件。那么_File对象调用generate方法返回的结果应该是整个模板文件映射成python代码的结果。这个结果是一个名叫_tt_execute的函数代码字符串。

函数中定义了两个变量。_tt_buffer,一个列表,列表中的每一项将会是各种类型的模板节点映射成python的代码。_tt_append,函数,实际上是_tt_buffer.append函数。

那么_tt_execute函数体的代码如何生成?

由self.body对象调用generate方法。self.body实际上是一个_ChunkList对象。

class _ChunkList(_Node):
    def __init__(self, chunks):
        self.chunks = chunks

    def generate(self, writer):
        for chunk in self.chunks:
            chunk.generate(writer)

    def each_child(self):
        return self.chunks

_ChunkList对象是父节点对象的子节点对象列表。

我们看到生成代码的过程,实际是节点森林自顶向下调用generate方法的过程,知道所用的叶子节点的generate方法调用完毕,才自底向上返回。整个过程也遍历了一遍节点森林。

整个过程,很多节点调用generate方法,都是直接拿”{% …%}”中含有的内容就作为生成的python代码。因此tornado的模板语言,具有极大的灵活性,它可以直接用到python语言中的很多语法。个别节点的generate方法生成代码,需要做特殊的处理。

模板继承

模板继承语法,让生成代码多了一些曲折。在tornado模板语法中,模板继承语法,主要由以下两个指令共同其作用:

指令 用法 对应_Node子类
block {% block name %}…{% end %} _NamedBlock
extends {% extends filename %} _ExtendsBlock

子模板,继承了一个父模板,父模板的命名块(_NamedBlock)将会被子模板中的命名块(_NamedBlock)替换。

这个过程,如果要生成代码该如何实现?

实现的原理,也很容易可以想到。我们对子模板生成代码,其实不应该调用子模板对应的_File对象的generate方法,反而应该调用的是这个模板继承链最顶级的父模板_File对象的generate方法

template模块中的Template类,提供了_get_ancestors方法。

def _get_ancestors(self, loader):
        ancestors = [self.file]
        for chunk in self.file.body.chunks:
            if isinstance(chunk, _ExtendsBlock):
                if not loader:
                    raise ParseError("{% extends %} block found, but no "
                                     "template loader")
                template = loader.load(chunk.name, self.name)
                ancestors.extend(template._get_ancestors(loader))
        return ancestors

那么又有另外一个问题,父模板中的命名块节点(_NamedBlock),应该被子模板同名的命名块节点替换,这个又该如何处理?

tornado对这个问题的处理代码如下,思路很简单,就留读者去思考了。

def _generate_python(self, loader, compress_whitespace):
        buffer = StringIO()
        try:
            # named_blocks maps from names to _NamedBlock objects
            named_blocks = {}
            ancestors = self._get_ancestors(loader)
            ancestors.reverse()
            for ancestor in ancestors:
                ancestor.find_named_blocks(loader, named_blocks)
            writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template,
                                 compress_whitespace)
            ancestors[0].generate(writer)
            return buffer.getvalue()
        finally:
            buffer.close()

编译与执行

第一步首先利用python内建函数compile,将_generate_python返回的python代码字符串编译成code对象。

code对象的执行主要在Template类的中的generate方法。

    def generate(self, **kwargs):
        namespace = {
            "escape": escape.xhtml_escape,
            "xhtml_escape": escape.xhtml_escape,
            "url_escape": escape.url_escape,
            "json_encode": escape.json_encode,
            "squeeze": escape.squeeze,
            "linkify": escape.linkify,
            "datetime": datetime,
            "_tt_utf8": escape.utf8,  # for internal use
            "_tt_string_types": (unicode_type, bytes),
            "__name__": self.name.replace(‘.‘, ‘_‘),
            "__loader__": ObjectDict(get_source=lambda name: self.code),
        }
        namespace.update(self.namespace)
        namespace.update(kwargs)
        exec_in(self.compiled, namespace)
        execute = namespace["_tt_execute"]
        linecache.clearcache()
        return execute()

exec_in是tornado util模块中的一个函数,主要是为了兼容python2和python3(在python2中exec作为语句使用,在python3以后,exec作为函数使用)。

从上面的代码,我们可以看到tornado自定义了一个执行上下文(namespace),提供了一些tornado模板中能够使用的基本函数,如“escape”,“xhtml_escape”等等。当然也会把,generate的参数(用于模板渲染)插入到namespace字典中。_tt_execute函数就在这样的一个namespace环境中执行,最后获得向客户端输出的内容页面字符串。

实际在真正的tornado web开发中,并不需要直接和Template对象打交道,而是通过web模块RequestHandler类的render方法和render_string方法,函数签名如下:

def render_string(self, template_name, **kwargs):
def render(self, template_name, **kwargs):

render_string与render有什么区别?render_string方法是根据一个模板的名字和字典参数,对模板进行渲染,返回的是一个内容页面字符串。而render方法会将渲染的结果,发送到客户端。

我们来看看render_string函数的实现。

    def render_string(self, template_name, **kwargs):
        template_path = self.get_template_path()
        if not template_path:
            frame = sys._getframe(0)
            web_file = frame.f_code.co_filename
            while frame.f_code.co_filename == web_file:
                frame = frame.f_back
            template_path = os.path.dirname(frame.f_code.co_filename)
        with RequestHandler._template_loader_lock:
            if template_path not in RequestHandler._template_loaders:
                loader = self.create_template_loader(template_path)
                RequestHandler._template_loaders[template_path] = loader
            else:
                loader = RequestHandler._template_loaders[template_path]
        t = loader.load(template_name)
        namespace = self.get_template_namespace()
        namespace.update(kwargs)
        return t.generate(**namespace)

render_string函数的作用,根据模板路径和模板名字加载指定的模板,加载模板,创建Template对象,最后调用Template对象的genereate方法。注意到传入generate方法的参数。namespace实际上首先是self.get_template_namespace()函数的返回结果。而get_template_namespace函数的实现如下:

def get_template_namespace(self):
        namespace = dict(
            handler=self,
            request=self.request,
            current_user=self.current_user,
            locale=self.locale,
            _=self.locale.translate,
            static_url=self.static_url,
            xsrf_form_html=self.xsrf_form_html,
            reverse_url=self.reverse_url
        )
        namespace.update(self.ui)
        return namespace

惊喜发现!原来在tornado的模板中还可以用到那么多tornado框架的对象和函数。

总结

本博文主要介绍tornado模板引擎实现原理,包括解析和编译。当然其中也简要介绍了模板语法,模板应用,涉及到数据结构与一些简单算法。tornado模板引擎实现非常简要,整个template模块代码不足千行,但也非常强大。

希望这篇文件对学习tornado模板的人有帮助。也愿意和读者交流学习,可以在csdn我的个人主页找到我的邮件联系方式。如果有转载,请注明来源http://blog.csdn.net/wyx819/article/details/45652713

时间: 2024-08-02 16:44:33

tornado模板引擎原理的相关文章

JavaScript模板引擎原理,几行代码的事儿

JavaScript模板引擎原理,几行代码的事儿 什么是模板引擎,说的简单点,就是一个字符串中有几个变量待定.比如: var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.'; 通过模板引擎函数把数据塞进去, var data = { "name": "Barret Lee", "age": "20" }; var result

smarty模板引擎原理解析

//php 控制器文件 <?php//引入模板引擎文件include("20130304.php");$smarty = new TinySmarty();$qq_numbers=array('a1'=>'12333','a2'=>'2222222','a3'=>'333333','a4'=>'3333333');$smarty->assign($qq_numbers);$smarty->assign('title','这是我的QQ号码');$

JavaScript模板引擎原理与用法

这篇文章主要介绍了JavaScript模板引擎原理与用法,结合实例形式详细分析了javascript模版引擎相关概念.原理.定义及使用方法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下 一.前言 什么是模板引擎,说的简单点,就是一个字符串中有几个变量待定.比如: var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.'; 通过模板引擎函数把数据塞进去, var data = { &q

js模板引擎原理,附自己写的简洁模板引擎

js处理DOM交互非常普遍,但DOM结构单纯用js字符串拼接简直难以维护,不方便理解和扩展. 下面展现了js模板引擎的实现原理: html中的模板 <script id="mytpl"> <div> 我的名字是:$name$ <br/> 今年$age$了! </div> </script> 因为script不会被浏览器解析和渲染,最大限度节省了浏览器资源,textarea标签同样可以达到效果.Template标签就是这样的目的

php模板引擎原理

模板引擎实现的原理 访问php文件, php文件会去加载模板引擎,通过模板引擎去加载模板然后替换模板里面的变量 然后生成一个编译文件 最后将该编译文件导入 访问的php文件中输出 第二次访问的时候 如果 缓存文件存在或者没有被改动则直接 导入缓存文件 输出 否则重新编译 自定义的一个模板引擎 mytpl.class.php <?php class mytpl{ //指定模板目录 private $template_dir; //编译后的目录 private $compile_dir; //读取模

高性能JavaScript模板引擎原理解析

随着 web 发展,前端应用变得越来越复杂,基于后端的 javascript(Node.js) 也开始崭露头角,此时 javascript 被寄予了更大的期望,与此同时 javascript MVC 思想也开始流行起来.javascript 模板引擎作为数据与界面分离工作中最重要一环,越来越受开发者关注,近一年来在开源社区中更是百花齐放,在 Twitter.淘宝网.新浪微博.腾讯QQ空间.腾讯微博等大型网站中均能看到它们的身影. 本文将用最简单的示例代码描述现有的 javascript 模板引擎

Trimpath Template 及JS模板引擎原理

这个是一个很老的JS模板引擎.看了下源码,理解下模拟条件语句的替换原则.感觉有点复杂~ 简记之. 如果不涉及条件语句,可以直接在代码里构建模板,ajax请求回来的数据套用即可.最原始方式. Trimpath JavaScript 是个轻量级的,基于JavaScript的,跨浏览器,采用APL/GPL开放源代码协议的,可以让你轻松进行基于模板编程方式的纯JS引擎. 它有如下的特点: 1.采用标准的JavaScript编写,支持跨浏览器 2.模板语法类似于:FreeMarker,Velocity,S

模板引擎原理及underscore.js使用

为什么要使用模板引擎 DOM结构简单,完全可以使用DOM方法创建DOM树.$("<td></td").appendTo(); 当页面比较复杂的时候,下面的程序中红色部分来自JSON中的数据: <div class="feeds-item hasImg" id="item-702635"> <p class="feeds-item-pic"> <a href="http:

tornado 模板引擎

在tornado的模板引擎中,有两种方式,UImethod与UImodule 自定义方法 在模板中调用方法: tornado:与Django一样使用{{}},但是对于for循环之类,Django以{% endfor%}结尾,而tornado以{% end%}结尾.调用字典或者列表,tornado使用list[0],而Django以list.0使用方法不一样. 要想使用UImethod与UImodule,必须先早app里面的settings中注册: import uimethod as udimp