tornado模板源碼小析

最近對flask的熱情有點下降,對tornado有點高漲。 之前在知乎上回答過一個問題,如何理解 Tornado ?,我的回答如下:

1.高性能的網絡庫,這可以和gevent,twisted,libevent等做對。

提供了異步io支持,超時事件處理,在此基礎上提供了tcpserver,httpclient,尤其是curlhttpclient,

在現有http客戶端中肯定排第一。可以用來做爬蟲,遊戲服務器,據我所知業界已有使用tornado作爲遊戲服務器

2.web框架,這可以和django,flask對。

提供了路由,模板等web框架必備組件。與其他區別是tornado是異步的,天然適合長輪訓,

這也是friendfeed發明tornado的原因,當前flask也可以支持,但必須藉助gevent等

3.較爲完備的http服務器,這點可以和nginx,apache對比,

但只支持http1.0,所以使用nginx做前段不僅是爲了更好利用多核,也是讓其支持http1.1

4.完備的wsgi服務器,這可以和gunicore,gevent wsgi server做對比,

也就是說可以讓flask運行在tornado之上,讓tornado加速flask

5.提供了完備的websocket支持,這讓html5的遊戲等提供了便利。

像知乎長輪訓就是使用了websocket,但websocket手機支持的不是很好,

前段時間不得不使用定時ajax發送大量請求,期待手機瀏覽器趕快奮起直追


最近研究了下tornado的模板,實現的比較簡潔,在這裏總結一下。

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> 

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


由於我們繼承了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)








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章