[译] 一个模板引擎是如何工作的?

ChandraProc 5年前
   <p>原文: <a href="/misc/goto?guid=4959676967783907735" rel="nofollow,noindex">How a template engine works</a></p>    <p>我已经使用模板引擎很长一段时间了,现在终于有时间来了解一下模板引擎是如何工作的。</p>    <h3>概述</h3>    <p>简单地说,模板引擎是一个工具,你可以用它来进行涉及到很多的文本数据的编程任务。最常见的用法是Web应用程序中的HTML生成。尤其是在Python中,如果你想要使用一个模板引擎,那么现在我们有几种选择,例如 <a href="/misc/goto?guid=4959667262277864823" rel="nofollow,noindex">jinja</a> 或者 <a href="/misc/goto?guid=4958973149662264617" rel="nofollow,noindex">mako</a> 。在这里,我们要通过深入tornado web框架的template模块,找出一个模板引擎是如何工作的,这是一个简单的系统,这样我们就可以专注于过程的基本思路。</p>    <p>进入实现细节之前,让我们首先来看看简单的API使用:</p>    <pre>  <code class="language-python">from tornado import template        PAGE_HTML = """      <html>        Hello, {{ username }}!        <ul>          {% for job in job_list %}            <li>{{ job }}</li>          {% end %}        </ul>      </html>      """      t = template.Template(PAGE_HTML)      print t.generate(username='John', job_list=['engineer'])</code></pre>    <p>这里,用户名在PAGE_HTML中是动态的,工作列表也是。你可以安装 tornado ,然后运行代码来看看输出。</p>    <h3>实现</h3>    <p>如果我们进一步看看 PAGE_HTML ,那我们很容易就可以发现,一个模板字符串有两个部分,静态文本部分和动态部分。我们使用特殊标记来区分开动态部分。总的来说,模板引擎应该接受模板字符串,然后原样输出静态部分,它还需要利用给定的上下文来处理动态部分,然后生成正确的字符串结果。所以,基本上,一个模板引擎就是一个Python函数:</p>    <pre>  <code class="language-python">def template_engine(template_string, **context):          # process here          return result_string</code></pre>    <p>在处理过程中,模板引擎有两个阶段:</p>    <ul>     <li><em>解析</em></li>     <li><em>渲染</em></li>    </ul>    <p>解析阶段接受模板字符串,然后生成可以渲染的结果。将模板字符串想成源代码的话,解析工具可以是一个编程语言解释器或者编程语言编译器。如果工具是解释器,那么解析会生成一个数据结构,而渲染工具将会根据这个结构来生成结果文本。Django模板引擎解析工具就是这么一个解释器。另外,解析生成一些可执行代码,那么渲染工具仅仅执行代码并生成结果。Jinja2, Mako和Tornado的template模块都使用编译器作为解析工具。</p>    <h3>编译</h3>    <p>如上所述,现在,我们需要解析模板字符串,而tornado的template模块中的解析工具将模板编译成Python代码。我们的解析工具只是一个Python函数,它生成Python代码:</p>    <pre>  <code class="language-python">def parse_template(template_string):          # compilation          return python_source_code</code></pre>    <p>在我们进入 parse_template 的实现之前,让我们看看它所生成的代码,下面是一个样例模板源字符串:</p>    <pre>  <code class="language-python"><html>        Hello, {{ username }}!        <ul>          {% for job in jobs %}            <li>{{ job.name }}</li>          {% end %}        </ul>      </html></code></pre>    <p>我们的 parse_template 函数将把这个模板编译成Python代码,它仅仅是一个函数,简化版本如下:</p>    <pre>  <code class="language-python">def _execute():          _buffer = []          _buffer.append('\n<html>\n  Hello, ')          _tmp = username          _buffer.append(str(_tmp))          _buffer.append('!\n  <ul>\n    ')          for job in jobs:              _buffer.append('\n      <li>')              _tmp = job.name              _buffer.append(str(_tmp))              _buffer.append('</li>\n    ')          _buffer.append('\n  </ul>\n</html>\n')          return ''.join(_buffer)</code></pre>    <p>现在,我们的模板被解析到一个名为 _execute 的函数中,该函数访问全局命名空间的所有上下文变量。该函数创建一个字符串列表,然后将它们连在一起变成结果字符串。 username 位于局部变量 _tmp 中,查询局部变量比查询全局变量快得多。这里,还可以做一些其他的优化,例如:</p>    <pre>  <code class="language-python">_buffer.append('hello')        _append_buffer = _buffer.append      # faster for repeated use      _append_buffer('hello')</code></pre>    <p>{{ ... }} 中的字符串被分析附加到字符串缓冲列表中。在tornado template模块中,对你的语句中可以包含的表达式并无限制,if和for块直接被转换成Python。</p>    <h3>代码</h3>    <p>现在,让我们看看实际实现。我们所使用的核心接口是 Template 类,当我们创建一个 Template 对象时,会对模板字符串进行编译,接着可以用它来渲染一个给定的上下文。我们仅需一次编译,就可以把该模板对象缓存起来,构造器的简化版本如下:</p>    <pre>  <code class="language-python">class Template(object):          def __init__(self, template_string):              self.code = parse_template(template_string)              self.compiled = compile(self.code, '<string>', 'exec')</code></pre>    <p>compile 会将 <em>source</em> 编译成一个code对象。稍后,我们可以使用一个 exec 语句来执行它。现在,构建 parse_template 函数,首先,我们需要将模板字符串解析成节点(node)列表,它清楚如何生成Python代码,我们需要一个名为 _parse 的函数,稍后我们将看到这个函数,我们现在需要一些辅助器,用以读取整个模板文件,我们有 _TemplateReader 类,在我们处理模板文件的时候,它为我们处理读取。我们需要从头开始,找到一些特殊的标记, _TemplateReader 将会记录当前位置,并为我们提供执行方法:</p>    <pre>  <code class="language-python">class _TemplateReader(object):          def __init__(self, text):              self.text = text              self.pos = 0            def find(self, needle, start=0, end=None):              pos = self.pos              start += pos              if end is None:                  index = self.text.find(needle, start)              else:                  end += pos                  index = self.text.find(needle, start, end)              if index != -1:                  index -= pos              return index            def consume(self, count=None):              if count is None:                  count = len(self.text) - self.pos              newpos = self.pos + count              s = self.text[self.pos:newpos]              self.pos = newpos              return s            def remaining(self):              return len(self.text) - self.pos            def __len__(self):              return self.remaining()            def __getitem__(self, key):              if key < 0:                  return self.text[key]              else:                  return self.text[self.pos + key]            def __str__(self):              return self.text[self.pos:]</code></pre>    <p>为了生成Python代码,我们需要 _CodeWriter 类,这个类编写代码行,并管理缩进,另外,它还是一个Python上下文管理器:</p>    <pre>  <code class="language-python">class _CodeWriter(object):          def __init__(self):              self.buffer = cStringIO.StringIO()              self._indent = 0            def indent(self):              return self            def indent_size(self):              return self._indent            def __enter__(self):              self._indent += 1              return self            def __exit__(self, *args):              self._indent -= 1            def write_line(self, line, indent=None):              if indent == None:                  indent = self._indent              for i in xrange(indent):                  self.buffer.write("    ")              print self.buffer, line            def __str__(self):              return self.buffer.getvalue()</code></pre>    <p>在 parse_template 的开头,我们首先创建一个模板读取器:</p>    <pre>  <code class="language-python">def parse_template(template_string):          reader = _TemplateReader(template_string)          file_node = _File(_parse(reader))          writer = _CodeWriter()          file_node.generate(writer)          return str(writer)</code></pre>    <p>然后,我们将读取器传递给 _parse 函数,并生成节点列表。所有这些节点都是模板文件节点的子节点。我们创建一个CodeWriter对象,文件节点将Python代码写入到CodeWriter中,然后返回生成的Python代码。 _Node 类将会处理特殊情况下的Python代码生成,稍后我们会看到。现在,回到 _parse 函数:</p>    <pre>  <code class="language-python">def _parse(reader, in_block=None):          body = _ChunkList([])          while True:              # Find next template directive              curly = 0              while True:                  curly = reader.find("{", curly)                  if curly == -1 or curly + 1 == reader.remaining():                      # EOF                      if in_block:                          raise ParseError("Missing {%% end %%} block for %s" %                                           in_block)                      body.chunks.append(_Text(reader.consume()))                      return body                  # If the first curly brace is not the start of a special token,                  # start searching from the character after it                  if reader[curly + 1] not in ("{", "%"):                      curly += 1                      continue                  # When there are more than 2 curlies in a row, use the                  # innermost ones.  This is useful when generating languages                  # like latex where curlies are also meaningful                  if (curly + 2 < reader.remaining() and                      reader[curly + 1] == '{' and reader[curly + 2] == '{'):                      curly += 1                      continue                  break</code></pre>    <p>进入无限循环以查找剩余文件中的模板指令,如果抵达文件末端,则附加文本节点并退出,否则,说明找到了一个模板指令。</p>    <pre>  <code class="language-python"># Append any text before the special token              if curly > 0:                  body.chunks.append(_Text(reader.consume(curly)))</code></pre>    <p>在我们处理了特殊的token后,如果有静态部分,则将其附加到文本节点。</p>    <pre>  <code class="language-python">start_brace = reader.consume(2)</code></pre>    <p>获取起始括号,它应该是 '{{' 或者 '{%' 。</p>    <pre>  <code class="language-python"># Expression              if start_brace == "{{":                  end = reader.find("}}")                  if end == -1 or reader.find("\n", 0, end) != -1:                      raise ParseError("Missing end expression }}")                  contents = reader.consume(end).strip()                  reader.consume(2)                  if not contents:                      raise ParseError("Empty expression")                  body.chunks.append(_Expression(contents))                  continue</code></pre>    <p>起始括号是 '{{' ,说明这里有一个表达式,仅需获取表达式内容,然后附加一个 _Expression 节点。</p>    <pre>  <code class="language-python"># Block              assert start_brace == "{%", start_brace              end = reader.find("%}")              if end == -1 or reader.find("\n", 0, end) != -1:                  raise ParseError("Missing end block %}")              contents = reader.consume(end).strip()              reader.consume(2)              if not contents:                  raise ParseError("Empty block tag ({% %})")              operator, space, suffix = contents.partition(" ")              # End tag              if operator == "end":                  if not in_block:                      raise ParseError("Extra {% end %} block")                  return body              elif operator in ("try", "if", "for", "while"):                  # parse inner body recursively                  block_body = _parse(reader, operator)                  block = _ControlBlock(contents, block_body)                  body.chunks.append(block)                  continue              else:                  raise ParseError("unknown operator: %r" % operator)</code></pre>    <p>这里,有一个块,通常,我们会递归获得块体,并附加一个 _ControlBlock 节点,而块体应该是一个节点列表。如果遇到一个 {% end %} ,则说明块结束了,退出该函数。</p>    <p>是时候找出 _Node 类的秘密了,它非常简单:</p>    <pre>  <code class="language-python">class _Node(object):          def generate(self, writer):              raise NotImplementedError()</code></pre>    <pre>  <code class="language-python">class _ChunkList(_Node):          def __init__(self, chunks):              self.chunks = chunks            def generate(self, writer):              for chunk in self.chunks:                  chunk.generate(writer)</code></pre>    <p>一个 _ChunkList 仅是一个节点列表。</p>    <pre>  <code class="language-python">class _File(_Node):          def __init__(self, body):              self.body = body            def generate(self, writer):              writer.write_line("def _execute():")              with writer.indent():                  writer.write_line("_buffer = []")                  self.body.generate(writer)                  writer.write_line("return ''.join(_buffer)")</code></pre>    <p>_File 节点将 _execute 函数写入到CodeWriter。</p>    <pre>  <code class="language-python">class _Expression(_Node):          def __init__(self, expression):              self.expression = expression            def generate(self, writer):              writer.write_line("_tmp = %s" % self.expression)              writer.write_line("_buffer.append(str(_tmp))")          class _Text(_Node):          def __init__(self, value):              self.value = value            def generate(self, writer):              value = self.value              if value:                  writer.write_line('_buffer.append(%r)' % value)</code></pre>    <p>_Text 和 _Expression 节点也相当简单,只是附加从模板源获得的东西。</p>    <pre>  <code class="language-python">class _ControlBlock(_Node):          def __init__(self, statement, body=None):              self.statement = statement              self.body = body            def generate(self, writer):              writer.write_line("%s:" % self.statement)              with writer.indent():                  self.body.generate(writer)</code></pre>    <p>对于一个 _ControlBlock 节点,我们需要缩进并带缩进编写子节点列表。</p>    <p>现在,让我们回到渲染部分,通过使用 Template 对象的 generate 方法,我们渲染一个上下文, generate 方法仅仅是调用编译好了的Python代码:</p>    <pre>  <code class="language-python">def generate(self, **kwargs):          namespace = {}          namespace.update(kwargs)          exec self.compiled in namespace          execute = namespace["_execute"]          return execute()</code></pre>    <p>exec 函数在给定的全局命名空间内执行编译好的代码,然后,从全局命名空间内抓取 _execute 函数,然后调用它。</p>    <h3>下一步</h3>    <p>就是这样啦,将模板编译成Python函数,然后执行以获取结果。tornado的template模块比我们这里讨论的特性要多得多,但我们已经了解到了基本思想,如果感兴趣的话,你可以发现更多:</p>    <ul>     <li>模板继承</li>     <li>模板包含</li>     <li>更多控制逻辑,例如else, elif, try, 等等</li>     <li>空格控制</li>     <li>转义</li>     <li>更多模板指令</li>    </ul>    <p> </p>    <p>来自:https://github.com/ictar/pythondocument/blob/master/Others/一个模板引擎是如何工作的?.md</p>    <p> </p>