天天看點

python基礎程式設計:深入解析Python的Tornado架構中内置的模闆引擎

模闆引擎是Web開發架構中負責前端展示的關鍵,這裡我們就來以執行個體深入解析Python的Tornado架構中内置的模闆引擎,來學習如何編寫Tonardo的模闆.

template中的_parse方法是模闆文法的解析器,而這個檔案中一坨一坨的各種node以及block,就是解析結果的承載者,也就是說在經過parse處理過後,我們輸入的tornado的html模闆就變成了各種block的集合。

這些block和node的祖宗就是這個“抽象”類, _Node,它定義了三個方法定義,其中generate方法是必須由子類提供實作的(是以我叫它“抽象”類)。

理論上來說,當一個類成為祖宗類時,必定意味着這個類包含了一些在子類中通用的行為,那麼,從_Node暴露出來的方法來看,即所有的子類理論上都會有如下特征:

  1. 可作為容器 (each_child, find_named_blocks)
  2. generate

    當然了,理想總是豐滿的,現實也總有那麼點兒不對勁,對于某些子孫,它們的特征看上去不是那麼靠譜,比如_Text。

    _Text這個類隻用到了generate這個方法,用于将文字(Html, JS)經過trim後添加到輸入流中,如果調用它的each_child or find_named_blocks,當然你能這麼做,但是沒有什麼意義。

    前面反複說到_Parse方法,它傳回的結果是一個_ChunkList的執行個體,而_ChunkList繼承與_Node。這是一個展現了_Node容器特點的類,重寫了generate方法和each_child方法,而基本上就是依次調用容器内所有元素的相關方法而已。

    _Nodes衆多子子孫孫中比較奇葩的是_ExtendsBlock這個類,丫什麼事情都沒做(That is true),看上去像是另外一個“抽象類”,但是居然會被_Parse初始化,用于處理Extends這個token(tornado術語)。我就納悶了,一旦這貨被generate,難道不會抛一個異常出來木?

    真正有意思的是另外幾個方法,它們有共通的模式,用_ApplyBlock來舉例

    在_ApplyBlock中,有趣的是generate方法

def generate(self, writer): 
  method_name = "apply%d" % writer.apply_counter 
  writer.apply_counter += 1
  writer.write_line("def %s():" % method_name, self.line) 
  with writer.indent(): 
    writer.write_line("_buffer = []", self.line) 
    writer.write_line("_append = _buffer.append", self.line) 
    self.body.generate(writer) 
    writer.write_line("return _utf8('').join(_buffer)", self.line) 
  writer.write_line("_append(%s(%s()))" % ( 
    self.method, method_name), self.line) 
           

簡單來說,這個函數做了兩件事情:

定義了一個python檔案全局函數叫做applyXXX():,其中的XXX是一個整形的,自增的值,傳回值是一個utf8字元串。

執行這個applyXXX函數,将此函數的輸出再作為self.method這個函數的輸入。

是以,如果一個類似于這樣的模闆

{%apply linkify%} {{address}} {%end%} 
           

會得到一個類似于如下的輸出:

r = applyXXX() 
r = linkify(r) 
_append(r) 
           

tornado的template機制,本質上講,就是允許開發者已HTML + template marker的方式來編寫視圖模闆,但是在背後,tornado會把這些視圖模闆通過template的處理,變成可編譯的python代碼。

拿autumn-sea上面的代碼作為例子,比較容易了解:

View Template

<html> 
  <head> 
    <title>{{ title }}</title> 
  </head> 
  <body> 
    hello! {{ name }} 
  </body> 
</html>
           

處理後_

buffer = [] 
_buffer.append('<html>\\n<head>\\n<title>') 
  
_tmp = title 
if isinstance(_tmp, str): _buffer.append(_tmp) 
elif isinstance(_tmp, unicode): _buffer.append(_tmp.encode('utf-8')) 
else: _buffer.append(str(_tmp)) 
  
_buffer.append('</title>\\n</head>\\n<body>\\n') 
_buffer.append('hello! ') 
  
_tmp = name 
if isinstance(_tmp, str): _buffer.append(_tmp) 
elif isinstance(_tmp, unicode): _buffer.append(_tmp.encode('utf-8')) 
else: _buffer.append(str(_tmp)) 
  
_buffer.append('\\n</body>\\n</html>\\n') 
return ''.join(_buffer)\n" 
           

執行個體剖析

tornado的模闆基本都在template.py這個檔案中,短短800多行代碼就實作了基本可用的模闆,讓我們慢慢揭開她的面紗。

首先我們看看tornado是如何編譯模闆的,下面是個簡單的模闆

t = Template("""\ 
{%if names%} 
  {% for name in names %} 
    {{name}} 
  {%end%} 
{%else%} 
no one 
{%end%} 
""") 
           

tornado最後編譯代碼如下:

def _tt_execute(): # <string>:0 
  _tt_buffer = [] # <string>:0 
  _tt_append = _tt_buffer.append # <string>:0 
  if names: # <string>:1 
    _tt_append('\n  ') # <string>:2 
    for name in names: # <string>:2 
      _tt_append('\n    ') # <string>:3 
      _tt_tmp = name # <string>:3 
      if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp) # <string>:3 
      else: _tt_tmp = _tt_utf8(str(_tt_tmp)) # <string>:3 
      _tt_tmp = _tt_utf8(xhtml_escape(_tt_tmp)) # <string>:3 
      _tt_append(_tt_tmp) # <string>:3 
      _tt_append('\n  ') # <string>:4 
      pass # <string>:2 
    _tt_append('\n') # <string>:5 
    pass # <string>:5 
  else: # <string>:5 
    _tt_append('\nno one\n') # <string>:7 
    pass # <string>:1 
  _tt_append('\n') # <string>:8 
  return _tt_utf8('').join(_tt_buffer) # <string>:0 
           

是的,你沒看錯,tornado編譯就是将之翻譯成一個個代碼塊,最後通exec傳遞我們給的參數命名空間執行_tt_execute函數。

在我們上面的模闆中包含了4種預定義的NODE節點,_ControlBlock,_Expression,_TEXT,每種Node節點都有自己的生成方式。

比如說_Expression表達式節點,也就是我們模闆中的{{name}},當_parse解析時發現’{‘後面還是’{'就認為是表達式節點,

class _Expression(_Node): 
  def __init__(self, expression, line, raw=False): 
    self.expression = expression 
    self.line = line 
    self.raw = raw 
  
  def generate(self, writer): 
    writer.write_line("_tt_tmp = %s" % self.expression, self.line) 
    writer.write_line("if isinstance(_tt_tmp, _tt_string_types):"
             " _tt_tmp = _tt_utf8(_tt_tmp)", self.line) 
    writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))", self.line) 
    if not self.raw and writer.current_template.autoescape is not None: 
      # In python3 functions like xhtml_escape return unicode, 
      # so we have to convert to utf8 again. 
      writer.write_line("_tt_tmp = _tt_utf8(%s(_tt_tmp))" %
               writer.current_template.autoescape, self.line) 
    writer.write_line("_tt_append(_tt_tmp)", self.line) 
           

最後生成時會調用節點的generate方法,self.expression就是上面的name,是以當exec的時候就會把name的值append到内部的清單中。

像if,for等都是控制節點,他們的定義如下:

class _ControlBlock(_Node): 
  def __init__(self, statement, line, body=None): 
    self.statement = statement 
    self.line = line 
    self.body = body 
  
  def each_child(self): 
    return (self.body,) 
  
  def generate(self, writer): 
    writer.write_line("%s:" % self.statement, self.line) 
    with writer.indent(): 
      self.body.generate(writer) 
      # Just in case the body was empty 
      writer.write_line("pass", self.line)
           

控制節點的generate方法有點意義,因為if,for等是下一行是需要縮進的,是以調用了with writer.indent繼續縮進控制,可以看下

_CodeWriter的indent方法。

節點中比較有意思的是_ExtendsBlock,這是實作目标基礎的節點,

class _ExtendsBlock(_Node): 
  def __init__(self, name): 
    self.name = name 
           

我們發現并沒有定義generate方法,那當生成繼承節點時不是會報錯嗎?讓我們看一段事例

loader = Loader('.') 
t=Template("""\ 
{% extends base.html %} 
{% block login_name %}hello world! {{ name }}{% end %} 
""",loader=loader) 
           

目前目錄下base.html如下:

<html>  
<head>  
<title>{{ title }}</title>  
</head>  
<body>  
{% block login_name %}hello! {{ name }}{% end %}  
</body>  
</html>
           

我們可以看看解析後的節點,

python基礎程式設計:深入解析Python的Tornado架構中内置的模闆引擎

由于我們繼承了base.html,是以我們的應該以base.html的模闆生成,并使用新定義的block代替base.html中的block,

這是很正常的思路,tornado也的确是這麼幹的,隻不過處理的并不是在_ExtendsBlock。

而實在Template的_generate_python中

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

_generate_python中調用_get_ancestors擷取目前模闆的父模闆,我們看到如果目前模闆的_FILE節點中有_ExtendsBlock就代表有父模闆并通過loader.load加載父模闆,此時父模闆已經是解析過的_FILE節點了。是以,在上面的模闆中,ancestors是[目前模闆_FILE節點,父模闆_FILE節點],ancestors.reverse()後其實ancestors[0]就是父模闆,我們看到最後是通過ancestors[0].generate(writer)來生成代碼的。那目前模闆是如何替換父模闆的block内容呢?

看上圖,block login_name通過解析為_NamedBlock,在_generate_python中通過調用ancestor.find_named_blocks來替換

父模闆的_NamedBlock的。

for ancestor in ancestors:
    ancestor.find_named_blocks(loader, named_blocks)
ancestor其實就是_FILE節點,find_named_blocks将周遊_FILE節點中所有節點并調用find_named_blocks
  
class _NamedBlock(_Node): 
  def find_named_blocks(self, loader, named_blocks): 
    named_blocks[self.name] = self
    _Node.find_named_blocks(self, loader, named_blocks) 
           

其它節點find_named_blocks都沒有做什麼事,_NamedBlock通過named_blocks[self.name] = self替換為目前模闆的_NamedBlock,因為ancestors父模闆在前,目前模闆在後,是以最後使用的是目前模闆的_NamedBlock。

生成代碼後generate将在給定的命名空間中exec代碼

def generate(self, **kwargs): 
  """Generate this template with the given arguments."""
  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_type), 
    # __name__ and __loader__ allow the traceback mechanism to find 
    # the generated source code. 
    "__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"] 
  # Clear the traceback module's cache of source data now that 
  # we've generated a new template (mainly for this module's 
  # unittests, where different tests reuse the same name). 
  linecache.clearcache() 
  return execute() 
           

是以在模闆中可以使用datetime等,都是通過在這裡注入到模闆中的,當然還有其它的是通過

web.py 中get_template_namespace注入的

def get_template_namespace(self): 
  """Returns a dictionary to be used as the default template namespace. 
 
  May be overridden by subclasses to add or modify values. 
 
  The results of this method will be combined with additional 
  defaults in the `tornado.template` module and keyword arguments 
  to `render` or `render_string`. 
  """
  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的模闆是如何對UI子產品的支援的。

{% for entry in entries %} 
 {% module Entry(entry) %} 
{% end %}
           

在使用module時将會生成_Module節點

class _Module(_Expression): 
  def __init__(self, expression, line): 
    super(_Module, self).__init__("_tt_modules." + expression, line, 
                   raw=True) 
           

我們看到其實_Module節點是繼承自_Expression節點,是以最後執行的是_tt_modules.Entry(entry)

_tt_modules定義在web.py的RequestHandler中

self.ui["_tt_modules"] = _UIModuleNamespace(self,application.ui_modules)
           

并通過上文的get_template_namespace中注入到模闆中。

class _UIModuleNamespace(object): 
  """Lazy namespace which creates UIModule proxies bound to a handler."""
  def __init__(self, handler, ui_modules): 
    self.handler = handler 
    self.ui_modules = ui_modules 
  
  def __getitem__(self, key): 
    return self.handler._ui_module(key, self.ui_modules[key]) 
  
  def __getattr__(self, key): 
    try: 
      return self[key] 
    except KeyError as e: 
      raise AttributeError(str(e)) 
           

是以當執行_tt_modules.Entry(entry)時先通路_UIModuleNamespace的__getattr__,後通路__getitem__,最後調用

handler._ui_module(key, self.ui_modules[key]),

def _ui_module(self, name, module): 
  def render(*args, **kwargs): 
    if not hasattr(self, "_active_modules"): 
      self._active_modules = {} 
    if name not in self._active_modules: 
      self._active_modules[name] = module(self) 
    rendered = self._active_modules[name].render(*args, **kwargs) 
    return rendered 
  return render 
           

_tt_modules.Entry(entry)中entry将會傳給_ui_module内部的render,也就是args=entry

self._active_modules[name] = module(self)此時就是執行個體化後的UIModule,調用render擷取渲染後的内容

class Entry(tornado.web.UIModule): 
  def render(self, entry, show_comments=False): 
    return self.render_string( 
      "module-entry.html", entry=entry, show_comments=show_comments)
           

當然如果你覺得這麼做費事,也可以使用tornado自帶的TemplateModule,它繼承自UIModule,

你可以這麼用

{% module Template("module-entry.html", show_comments=True) %} 
           

在module_entry.html中可以通過set_resources引用需要的靜态檔案

{{ set_resources(embedded_css=".entry { margin-bottom: 1em; }") }} 
           

這裡需要注意的是:隻能在Template引用的html檔案中使用set_resources函數,因為set_resources是TemplateModule.render的内部函數

class TemplateModule(UIModule): 
  """UIModule that simply renders the given template. 
  
  {% module Template("foo.html") %} is similar to {% include "foo.html" %}, 
  but the module version gets its own namespace (with kwargs passed to 
  Template()) instead of inheriting the outer template's namespace. 
  
  Templates rendered through this module also get access to UIModule's 
  automatic javascript/css features. Simply call set_resources 
  inside the template and give it keyword arguments corresponding to 
  the methods on UIModule: {{ set_resources(js_files=static_url("my.js")) }} 
  Note that these resources are output once per template file, not once 
  per instantiation of the template, so they must not depend on 
  any arguments to the template. 
  """
  def __init__(self, handler): 
    super(TemplateModule, self).__init__(handler) 
    # keep resources in both a list and a dict to preserve order 
    self._resource_list = [] 
    self._resource_dict = {} 
  
  def render(self, path, **kwargs): 
    def set_resources(**kwargs): 
      if path not in self._resource_dict: 
        self._resource_list.append(kwargs) 
        self._resource_dict[path] = kwargs 
      else: 
        if self._resource_dict[path] != kwargs: 
          raise ValueError("set_resources called with different "
                   "resources for the same template") 
      return "" 
    return self.render_string(path, set_resources=set_resources, 
                 **kwargs)
           

最後給大家推薦一個資源很全的python學習聚集地,[點選進入],這裡有我收集以前學習心得,學習筆

記,還有一線企業的工作經驗,且給大定on零基礎到項目實戰的資料,大家也可以在下方,留言,把不

懂的提出來,大家一起學習進步