Manjusaka

Manjusaka

詳解模板引擎工作機制

我已經使用各種模板引擎很久了,現在終於有時間研究一下模板引擎到底是如何工作的了。

簡介#

簡單的說,模板引擎是一種可以用來完成涉及大量文本數據的編程任務的工具。一般而言,我們經常在一個 web 應用中利用模板引擎來生成 HTML 。在 Python 中,當你想使用模板引擎的時候,你會發現你有不少的選擇,比如jinja 或者是mako。從現在開始,我們將利用 tornado 中的模板引擎來講解模板引擎的工作原理,在 tornado 中,自帶的模板引擎相對的簡單,能方便我們去深入的剖析其原理。

在我們研究(模板引擎)的實現原理之前,先讓我們來看一個簡單的接口調用例子。

    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'])

這段代碼裡的 username 將會動態的生成,job 列表也是如此。你可以通過安裝 tornado 並運行這段代碼來看看最後的效果。

詳解#

如果你仔細觀察 PAGE_HTML ,你會發現這段模板字符串由兩個部分組成,一部分是固定的字符串,另一部分是將會動態生成的內容。我們將會用特殊的符號來標註動態生成的部分。在整個工作流程中,模板引擎需要正確輸出固定的字符串,同時需要將正確的結果替換我們所標註的需要動態生成的字符串。

使用模板引擎最簡單的方式就是像下面這樣用一行 python 代碼就可以解決:

    deftemplate_engine(template_string, **context):# process herereturn result_string

在整個工作過程中,模板引擎將會分為如下兩個階段對我們的字符串進行操作:

  • 解析
  • 渲染

在解析階段,我們將我們準備好的字符串進行解析,然後格式化成可被渲染的格式,其可能是能被 rendered.Consider 所解析的字符串,解析器可能是一種語言的解釋器或是一個語言的編譯器。如果解析器是一種解釋器的話,在解析過程中將會生成一種特殊的數據結構來存放數據,然後渲染器會遍歷整個數據結構來進行渲染。例如 Django 的模板引擎中的解析器就是一種基於解釋器的工具。除此之外,解析器可能會生成一些可執行代碼,渲染器將只會執行這些代碼,然後生成對應的結果。在 Jinja2MakoTornado 中,模板引擎都在使用編譯器來作為解析工具。

編譯#

如同上面所說的一樣,我們需要解析我們所編寫的模板字符串,然後 tornado 中的模板解析器將會將我們所編寫的模板字符串編譯成可執行的 Python 代碼。我們的解析工具負責生成 Python 代碼,而僅僅由單個 Python 函數構成:

    def parse_template(template_string):
      # compilation
      return python_source_code

在我們分析 parse_template 的代碼之前,讓我們先看個模板字符串的例子:

    <html>
      Hello, { { username } }!
      <ul>
        { % for job in jobs % }
          <li>{ { job.name } }</li>
        { % end % }
      </ul>
    </html>

模板引擎裡的 parse_template 函數將會將上面這個字符串編譯成 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)

現在我們在 _execute 函數裡處理我們的模板。這個函數將可以使用全局命名空間裡的所有有效變量。這個函數將創建一個包含多個 string 的列表並將它們合併後返回。顯然找到一個局部變量比找一個全局變量要快多了。同時,我們對其餘代碼的優化也在這個階段完成,比如:

    _buffer.append('hello')

    _append_buffer = _buffer.append
    # faster for repeated use
    _append_buffer('hello')

{ { ... } } 中的表達式將會被提取出來,然後添加進 string 列表中。在 tornado 模板模塊中,在 { { ... } } 所編寫的表達式沒有任何的限制,iffor 代碼塊都可以準確地轉換成為 Python 代碼。

讓我們來看看具體的代碼實現吧#

讓我們來看看模板引擎的具體實現吧。我們在 Template 類中編聲明核心變量,當我們創建一個 Template 對象後,我們便可以編譯我們所編寫的模板字符串,隨後我們便可以根據編譯的結果來對其進行渲染。我們只需要對我們所編寫的模板字符串進行一次編譯,然後我們可以緩存我們的編譯結果,下面是 Template 類的簡化版本的構造器:

    class Template(object):
        def__init__(self, template_string):
            self.code = parse_template(template_string)
            self.compiled = compile(self.code, '<string>', 'exec')

上段代碼裡的 compile 函數將會將字符串編譯成為可執行代碼,我們可以稍後調用 exec 函數來執行我們生成的代碼。現在,讓我們來看看 parse_template 函數的實現,首先,我們需要將我們所編寫的模板字符串轉化成一個個獨立的節點,為我們後面生成 Python 代碼做好準備。在這過程中,我們需要一個 _parse 函數,我們先把它放在一邊,等下再回來看看這個函數。現,我們需要編寫一些輔助函數來幫助我們從模板文件裡讀取數據。現在讓我們來看看 _TemplateReader 這個類,它用於從我們自定義的模板中讀取數據:

    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:]

為了生成 Python 代碼,我們需要去看看 _CodeWriter 這個類的源碼,這個類可以編寫代碼行和管理縮進,同時它也是一個 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()

parse_template 函數裡,我們先要創建一個 _TemplateReader 對象:

    def parse_template(template_string):
        reader = _TemplateReader(template_string)
        file_node = _File(_parse(reader))
        writer = _CodeWriter()
        file_node.generate(writer)
        return str(writer)

然後,我們將我們所創建的 _TemplateReader 對象傳入 _parse 函數中以便生成節點列表。這裡生成的所有節點都是模板文件的子節點。接著,我們創建一個 _CodeWriter 對象,然後 file_node 對象會把生成的 Python 代碼寫入 _CodeWriter 對象中。然後我們返回一系列動態生成的 Python 代碼。_Node 類將會用一種特殊的方法去生成 Python 源碼。這個先放著,我們等下再繞回來看看。 現在先讓我們回頭看看前面所說的 _parse 函數:

    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

我們將在文件中無限循環下去來查找我們所規定的特殊標記符號。當我們到達文件的末尾處時,我們將文本節點添加至列表中然後退出循環。

    # Append any text before the special token
    if curly > 0:
      body.chunks.append(_Text(reader.consume(curly)))

在我們對特殊標記的代碼塊進行處理之前,我們先將靜態的部分添加至節點列表中。

    start_brace = reader.consume(2)

在遇到 { { 或者 { % 的符號時,我們便開始著手處理相應的的表達式:

    # 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

當遇到 { { 之時,便意味著後面會跟隨一個表達式,我們只需要將表達式提取出來,並添加至 _Expression 節點列表中。

      # 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)

在遇到模板裡的代碼塊的時候,我們需要通過遞歸的方式將代碼塊提取出來,並添加至 _ControlBlock 節點列表中。當遇到 { % end % } 時,意味著這個代碼塊的結束,這個時候我們可以跳出相對應的函數了。

好了現在,讓我們看看之前所提到的 _Node 節點,別慌,這其實是很簡單的:

    class _Node(object):
      def generate(self, writer):
          raise NotImplementedError()


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

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

_ChunkList 只是個節點列表而已。

    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)")

_File 中,它會將 _execute 函數寫入 CodeWriter

    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)

_Text_Expression 節點的實現也非常簡單,它們只是將我們從模板裡獲取的數據添加進列表中。

    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)

_ControlBlock 中,我們需要將我們獲取的代碼塊按 Python 語法進行格式化。

現在讓我們看看之前所提到的模板引擎的渲染部分,我們通過在 Template 對象中實現 generate 方法來調用從模板中解析出來的 Python 代碼。

    def generate(self, **kwargs):
        namespace = { }
        namespace.update(kwargs)
        exec self.compiled in namespace
        execute = namespace["_execute"]
        return execute()

在給予的全局命名空間中, exec 函數將會執行編譯過的代碼對象。然後我們就可以在全局中調用 _execute 函數了。

最後#

經過上面的一系列操作,我們便可以盡情的編譯我們的模板並得到相對應的結果了。其實在 tornado 模板引擎中,還有很多特性是我們沒有討論到的,不過,我們已經了解了其最基礎的工作機制,你可以在此基礎上去研究你所感興趣的部分,比如:

  • 模板繼承
  • 模板包含
  • 其餘的一些邏輯控制語句,比如 else , elfi , try 等等
  • 空白控制
  • 特殊字符轉譯
  • 更多沒講到的模板指令(譯者注:請參考 tornado 官方文檔
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。