web.py源码分析: 模板(3)

前两篇文章主要说明了web.py的模板系统将模板文件处理后得到的结果:__template__()函数。本文主要讲述模板文件是如何变成__template__()函数的。

Render和frender

一般来说,更常用的是Render类,该类会处理整个目录下的模板,还支持缓存和嵌套模板。不过这些其实都和模板本身的解析基本没关系,以后再说明这个类的实现和用途。这里我们使用frender()函数:

这个函数相当简单,只作了一键事情,就是读取模板文件内容,然后交给Template类处理,并且返回一个Template类实例。从这里也可以看出,整个模板的解析,只和Template类有关,frender是来打杂的。

Template类

Template实例的效果

当我们根据一个模板内容创建一个Template类实例t后,我们可以调用该实例,这相当于调用模板对应的__template__()函数,得到的结果是一个TemplateResult实例。

Template实例化过程

Template实例化过程是把模板转换成HTML内容的实质性步骤,不过这个过程比较复杂。但是,概括的来讲,这个过程和Template的__init__()函数中的步骤差不都差不多。

首先把,参数里除了text以外的参数忽略掉,然后来看下对text的处理过程(text就是模板的内容)。整个过程概括的说有如下步骤:

  • 对text作归一化处理:text = Template.normalize_text(text), 主要换行符统一成n,删除BOM字符串,将$替换成$$,这个就是简单的字符串处理。
  • 编译模板得到编译后的Python字节码codecode = self.compile_template(text, filename),code就是之前已经提到过的__template__()函数。
  • 调用父类(BaseTemplate)的__init__()函数:创建__template__()函数的执行环境,并且实现可调用功能。

其他没有说明的代码,暂时都可以忽略,不会影响你理解Template的实例化过程。从上面的步骤可以看出,Template实例化的过程主要有两个:生成__template__()函数的代码并编译,以及创建__template__()函数的执行环境。

生成__template__()函数的代码

这个是模板生成过程中最长最复杂的一段,会应用到Python的token分析功能以及动态编译功能。还记得第一篇里,我们搭建实验环境的时候,修改了web.py源码,在一个地方插入了一行打印语句么?没错,就是这个compile_template()函数,我们现在来看看它是如何生成__template__()函数的代码的。

很明显,第一行的调用就生成了__template__()函数的代码。我们继续看:

这两个函数配合起来使用,意思就是:创建一个没有扩展的Parser实例,用该实例解析模板内容得到rootnode,调用rootnodeemit()函数得到最终的代码(__template__()函数)。所以,问题的关键由转到了Parser类上。

Parser类初探

Parser类的parse函数解析模板内容,然后返回一些节点结构,这种节点结构的emit方法会产生实际的Python代码。

上面的代码是Template类中用的Parser类的两个方法,初始化函数没啥好说的,parse函数则是说明了模板解析的最顶层逻辑:

  1. 先解析def with这行。
  2. 然后解析剩余部分。
  3. 最后返回一个DefwithNode实例。

这里不打算细说整个解析过程(后面专门说),反正知道了Parser类的parse函数可以返回一个DefwithNode实例,且调用其emit()方法后能生成最终的代码即可。

编译__template__()函数的代码

compile_template方法得到模板对应的代码后,就要对这个代码进行编译,生成Python的字节码:

到此就完成了Template类实例化过程的第二个步骤:得到了编译后的模板函数代码。

BaseTemplate

接下来就是要调用父类,也就是BaseTemplate类,的初始化方法了:

该方法接受的参数中,code就是上面编译出来的字节码,globalsbuiltins是用来构建__template__()函数的运行环境的,记得在web.py的文档中有说可以修改这两个的地方么?忘记的话,传送门filter函数是过滤函数,用来处理生成的HTML内容。

globals和builtins

如果你的代码中不指定,默认情况下,globals是空的,builtins则包含如下内容:

这些就是Python的全局内置函数的一个子集,你在模板中可以直接使用。

BaseTemplate的初始化

这个初始化过程,我们直接看代码(只列出相关函数):

初始化的时候,主要的工作是调用self.t = self._compile(code)_compile()方法先使用make_env()方法构造一个函数运行环境env,里面包含了__template__()函数会用到的对象,包括默认的内置函数,以及TemplateResult等。然后,调用exec(code, env)env环境下执行__template__()函数的定义,最后返回的env['__template']就是在env环境下执行的__template__()函数。

注意:*在python2中,exec是一个语句:

The first expression should evaluate to either a Unicode string, a Latin-1 encoded string, an open file object, a code object, or a tuple.

exec还有下面这种形式:

如果以元组作为参数,则有如下两个形式:

globals字典中可以插入__builtins__ 对象来设置内置对象的引用,比如上面的make_env()函数。

Parser类

现在可以来看Parser类是如何解析模板,并生成特定的节点结构的。还是从parse函数开始:

首先调用read_defwith()方法,将$def with (name)这行分离出来,剩余的都交给read_suite()方法去解析,最后再实例化一个DefwithNode作为解析结果。整个解析的主要工作是由read_suite()方法完成的。

Parser类的实现惯例

解析函数

Parser类里面实现了如下这些方法:

这些方法的参数和返回值都遵循同一个模式,先知道一下有助于阅读代码。

  • 每个方法都负责解析特定的内容(方法名可以体现),有的方法内部会调用其他的方法。
  • 参数都是一个text,表示还未解析的模板内容。
  • 返回值都是含有两个元素的元组,第一个元素是该函数解析的结果(字符串或者某个类实例),第二个元素是该函数处理过后还剩余的模板内容(给下个函数去解析)。

从这个惯例可以看出,整个模板解析的思想是:从模板内容头部开始,一点一点的读取,一旦所读取的内容可以解析就进行解析,剩余的内容再继续这么处理

另外,这些解析函数还有包含关系,有一些是处理整块内容的函数,有一些则是处理一行,还有的是处理一个单词,这也就形成了一个调用关系:粗粒度的函数调用细粒度的解析函数。如下图所示:

解析节点

上一小节提到了,解析函数返回值中的第一个一般都是解析好的节点类实例。那么什么是解析节点呢?解析节点其实就是一些类,这些类都实现了emit()函数,当调用emit()时,就会产生对应的代码字符串。先来看下有哪些解析节点吧:

这些节点的功能基本上从类名称就可以看出来,有些节点只是其他节点的包装(比如LineNode),有一些则需要处理比较复杂的情况(比如ForNode)。但是这些节点作的事情都是可以概括为:处理初始化参数以便在调用emit方法的时候能产生正确的代码。来看两个例子:

AssignmentNode

模板内容

函数内容

我们知道当解析到$ b = name这行时,会生成一个AssignmentNode,调用read_assignment()的代码在Parser类的read_section()方法里,我们可以模拟一下:

web.py的模板系统就是这样针对每个节点调用emit()方法来生产最终的代码的。

ForNode

我们再来看复杂一点的ForNode节点。

模板内容

函数内容

web.py中调用创建ForNode实例的方法是read_block_section(),我们可以这么模拟:

创建ForNode节点实例时,是把第一行的循环控制语句和其他的循环内部代码分别作为两个参数传递给ForNode的初始化函数。ForNode的主要工作是解析第一个参数,转换为loop.setup()这样的代码(为了能够在模板中支持loop关键字)。然后,调用父类BlockNode的初始化函数,主要作用是调用Parser类的read_suite()方法解析循环内部的代码,因为我们在循环内部也会使用模板语法(就像上面例子中的$namen被转换成extend_([u' ', escape_(name, True), u'n']))。

SuiteNode

SuiteNode内部就是一个列表,保存了所有子节点,调用SuiteNode的emit()方法时,就是依次调用子节点的emit()方法,然后连接成一个字符串:

DefwithNode

DefwithNode作为根节点,做了两个事情:

  • 生成__template__()函数的框架。
  • 调用suite.emit()生成其余的代码,并且拼接得到一个完整的函数。

Parser类小结

本章讲解了Parser类的大部分实现,但是没有讲解Parser类如何分析模板内容来确定生成哪个节点。这部分的内容都是具体的实现细节,采用的分析技术主要有两种:

  • 字符串分析,看看字符串是否以某些特定模式开始。
  • token分析,利用了Python的tokenize模块的功能来分析。

总结

本文主要是分析了从模板文件到Template类实例的生成过程,大概是如下几个步骤:

  • 调用frender()函数读取模板文件内容,作为参数传递给Template类的初始化函数。
  • Template类调用Parser类将模板内容解析成__template__()函数的定义代码。
  • Template类调用Python的compile函数将生成的__template__()定义代码进行编译。
  • Template类调用父类BaseTemplate类的初始化函数构建__template__()函数的执行环境,得到在指定环境下执行的__template__()函数。
1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部