上一篇《白話tornado源碼之請求來了》介紹了客戶端請求在tornado框架中的生命周期,其本質就是利用epoll和socket來獲取並處理請求。在上一篇的內容中,我們只是給客戶端返回了簡單的字符串,如:“Hello World”,而在實際開發中,需要使用html文件的內容作為模板,然后將被處理后的數據(計算或數據庫中的數據)嵌套在模板中,然后將嵌套了數據的html文件的內容返回給請求者客戶端,本篇就來詳細的剖析模板處理的整個過程。
概述
上圖是返回給用戶一個html文件的整個流程,較之前的Demo多了綠色流線的步驟,其實就是把【self.write('hello world')】變成了【self.render('main.html')】,對於所有的綠色流線只做了五件事:
- 使用內置的open函數讀取Html文件中的內容
- 根據模板語言的標簽分割Html文件的內容,例如:{{}} 或 {%%}
- 將分割后的部分數據塊格式化成特殊的字符串(表達式)
- 通過python的內置函數執行字符串表達式,即:將html文件的內容和嵌套的數據整合
- 將數據返回給請求客戶端
所以,如果要返回給客戶端對於一個html文件來說,根據上述的5個階段其內容的變化過程應該是這樣:
class MainHandler(tornado.web.RequestHandler): def get(self): self.render("main.html",**{'data':['11','22','33'],'title':'main'}) [main.html] <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <h1>{{title}}</h1> {% for item in data %} <h3>{{item}}</h3> {% end %} </body> </html>
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <h1>{{title}}</h1> {% for item in data %} <h3>{{item}}</h3> {% end %} </body> </html>
第1塊:'<!DOCTYPE html><html><head lang="en"><meta charset="UTF-8"><title></title></head><h1>' 第2塊:'title' 第3塊:'</h1> \n\n' 第4塊:'for item in data' 第4.1塊:'\n <h3>' 第4.2塊:'item' 第4.3塊:'</h3> \n' 第五塊:'</body>'
'def _execute(): _buffer = [] _buffer.append(\\'<!DOCTYPE html>\\n<html>\\n<head lang="en">\\n<meta charset="UTF-8">\\n<title></title>\\n</head>\\n<body>\\n<h1>\\') _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(\\'</h1>\\n\\') for item in data: _buffer.append(\\'\\n<h3>\\') _tmp = item if isinstance(_tmp, str): _buffer.append(_tmp) elif isinstance(_tmp, unicode): _buffer.append(_tmp.encode(\\'utf-8\\')) else: _buffer.append(str(_tmp)) _buffer.append(\\'</h3>\\n\\') _buffer.append(\\'\\n</body>\\n</html>\\') return \\'\\'.join(_buffer) '
a、參照本篇博文的前戲 http://www.cnblogs.com/wupeiqi/p/4592637.html b、全局變量有 title = 'main';data = ['11','22','33']
在第4步中,執行第3步生成的字符串表示的函數后得到的返回值就是要返回給客戶端的響應信息主要內容。
3.13、RequestHandler的render方法
此段代碼主要有三項任務:
- 獲取Html文件內容並把數據(程序數據或框架自帶數據)嵌套在內容中的指定標簽中(本篇主題)
- 執行ui_modules,再次在html中插入內容,例:head,js文件、js內容、css文件、css內容和body
- 內部調用客戶端socket,將處理請求后的數據返回給請求客戶端
class RequestHandler(object):
def render(self, template_name, **kwargs):
#根據Html文件名稱獲取文件內容並把參數kwargs嵌入到內容的指定標簽內
html = self.render_string(template_name, **kwargs)
#執行ui_modules,再在html的內容中插入head,js文件、js內容、css文件、css內容和body信息。
js_embed = []
js_files = []
css_embed = []
css_files = []
html_heads = []
html_bodies = []
for module in getattr(self, "_active_modules", {}).itervalues():
embed_part = module.embedded_javascript()
if embed_part: js_embed.append(_utf8(embed_part))
file_part = module.javascript_files()
if file_part:
if isinstance(file_part, basestring):
js_files.append(file_part)
else:
js_files.extend(file_part)
embed_part = module.embedded_css()
if embed_part: css_embed.append(_utf8(embed_part))
file_part = module.css_files()
if file_part:
if isinstance(file_part, basestring):
css_files.append(file_part)
else:
css_files.extend(file_part)
head_part = module.html_head()
if head_part: html_heads.append(_utf8(head_part))
body_part = module.html_body()
if body_part: html_bodies.append(_utf8(body_part))
if js_files:#添加js文件
# Maintain order of JavaScript files given by modules
paths = []
unique_paths = set()
for path in js_files:
if not path.startswith("/") and not path.startswith("http:"):
path = self.static_url(path)
if path not in unique_paths:
paths.append(path)
unique_paths.add(path)
js = ''.join('<script src="' + escape.xhtml_escape(p) +
'" type="text/javascript"></script>'
for p in paths)
sloc = html.rindex('</body>')
html = html[:sloc] + js + '\n' + html[sloc:]
if js_embed:#添加js內容
js = '<script type="text/javascript">\n//<![CDATA[\n' + \
'\n'.join(js_embed) + '\n//]]>\n</script>'
sloc = html.rindex('</body>')
html = html[:sloc] + js + '\n' + html[sloc:]
if css_files:#添加css文件
paths = []
unique_paths = set()
for path in css_files:
if not path.startswith("/") and not path.startswith("http:"):
path = self.static_url(path)
if path not in unique_paths:
paths.append(path)
unique_paths.add(path)
css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
'type="text/css" rel="stylesheet"/>'
for p in paths)
hloc = html.index('</head>')
html = html[:hloc] + css + '\n' + html[hloc:]
if css_embed:#添加css內容
css = '<style type="text/css">\n' + '\n'.join(css_embed) + \
'\n</style>'
hloc = html.index('</head>')
html = html[:hloc] + css + '\n' + html[hloc:]
if html_heads:#添加html的header
hloc = html.index('</head>')
html = html[:hloc] + ''.join(html_heads) + '\n' + html[hloc:]
if html_bodies:#添加html的body
hloc = html.index('</body>')
html = html[:hloc] + ''.join(html_bodies) + '\n' + html[hloc:]
#把處理后的信息響應給客戶端
self.finish(html)
對於上述三項任務,第一項是模板語言的重中之重,讀取html文件並將數據嵌套到指定標簽中,以下的步驟用於剖析整個過程(詳情見下文);第二項是對返會給用戶內容的補充,也就是在第一項處理完成之后,利用ui_modules再次在html中插入內容(head,js文件、js內容、css文件、css內容和body);第三項是通過socket將內容響應給客戶端(見上篇)。
對於ui_modules,每一個ui_module其實就是一個類,一旦注冊並激活了該ui_module,tornado便會自動執行其中的方法:embedded_javascript、javascript_files、embedded_css、css_files、html_head、html_body和render ,從而實現對html內容的補充。(執行過程見上述代碼)
自定義UI Modules
此處是一個完整的 創建 --> 注冊 --> 激活 的Demo
目錄結構:
├── index.py
├── static
└── views
└── index.html
#!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.ioloop import tornado.web class CustomModule(tornado.web.UIModule): def embedded_javascript(self): return 'embedded_javascript' def javascript_files(self): return 'javascript_files' def embedded_css(self): return 'embedded_css' def css_files(self): return 'css_files' def html_head(self): return 'html_head' def html_body(self): return 'html_body' def render(self): return 'render' class MainHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') settings = { 'static_path': 'static', "template_path": 'views', "ui_modules": {'Foo': CustomModule}, } application = tornado.web.Application([(r"/", MainHandler), ], **settings) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <hr> {% module Foo() %} <hr> </body> </html>
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <!-- css_files --> <link href="/static/css_files" type="text/css" rel="stylesheet"> <!-- embedded_css --> <style type="text/css"> embedded_css </style> </head> <body> <!-- html_head --> html_head <hr> <!-- redner --> render <hr> <!-- javascript_files --> <script src="/static/javascript_files" type="text/javascript"></script> <!-- embedded_javascript --> <script type="text/javascript"> //<![CDATA[ embedded_javascript //]]> </script> <!-- html_body --> html_body </body> </html>
3.13.1~6、RequestHandler的render_string方法
該方法是本篇的重中之重,它負責去處理Html模板並返回最終結果,【概述】中提到的5件事中前四件都是此方法來完成的,即:
- 創建Loader對象,並執行load方法
-- 通過open函數打開html文件並讀取內容,並將內容作為參數又創建一個 Template 對象
-- 當執行Template的 __init__ 方法時,根據模板語言的標簽 {{}}、{%%}等分割並html文件,最后生成一個字符串表示的函數 - 獲取所有要嵌入到html模板中的變量,包括:用戶返回和框架默認
- 執行Template對象的generate方法
-- 編譯字符串表示的函數,並將用戶定義的值和框架默認的值作為全局變量
-- 執行被編譯的函數獲取被嵌套了數據的內容,然后將內容返回(用於響應給請求客戶端)
注意:詳細編譯和執行Demo請參見《第四篇:白話tornado源碼之褪去模板外衣的前戲 》
class RequestHandler(object):
def render_string(self, template_name, **kwargs):
#獲取配置文件中指定的模板文件夾路徑,即:template_path = 'views'
template_path = self.get_template_path()
#如果沒有配置模板文件的路徑,則默認去啟動程序所在的目錄去找
if not template_path:
frame = sys._getframe(0)
web_file = frame.f_code.co_filename
while frame.f_code.co_filename == web_file:
frame = frame.f_back
template_path = os.path.dirname(frame.f_code.co_filename)
if not getattr(RequestHandler, "_templates", None):
RequestHandler._templates = {}
#創建Loader對象,第一次創建后,會將該值保存在RequestHandler的靜態字段_template_loaders中
if template_path not in RequestHandler._templates:
loader = self.application.settings.get("template_loader") or\
template.Loader(template_path)
RequestHandler._templates[template_path] = loader
#執行Loader對象的load方法,該方法內部執行執行Loader的_create_template方法
#在_create_template方法內部使用open方法會打開html文件並讀取html的內容,然后將其作為參數來創建一個Template對象
#Template的構造方法被執行時,內部解析html文件的內容,並根據內部的 {{}} {%%}標簽對內容進行分割,最后生成一個字符串類表示的函數並保存在self.code字段中
t = RequestHandler._templates[template_path].load(template_name)
#獲取所有要嵌入到html中的值和框架默認提供的值
args = 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.application.reverse_url
)
args.update(self.ui)
args.update(kwargs)
#執行Template的generate方法,編譯字符串表示的函數並將namespace中的所有key,value設置成全局變量,然后執行該函數。從而將值嵌套進html並返回。
return t.generate(**args)
class Loader(object): """A template loader that loads from a single root directory. You must use a template loader to use template constructs like {% extends %} and {% include %}. Loader caches all templates after they are loaded the first time. """ def __init__(self, root_directory): self.root = os.path.abspath(root_directory) self.templates = {}
class Loader(object): def load(self, name, parent_path=None): name = self.resolve_path(name, parent_path=parent_path) if name not in self.templates: path = os.path.join(self.root, name) f = open(path, "r") #讀取html文件的內容 #創建Template對象 #name是文件名 self.templates[name] = Template(f.read(), name=name, loader=self) f.close() return self.templates[name]
class Template(object): def __init__(self, template_string, name="<string>", loader=None,compress_whitespace=None): # template_string是Html文件的內容 self.name = name if compress_whitespace is None: compress_whitespace = name.endswith(".html") or name.endswith(".js") #將內容封裝到_TemplateReader對象中,用於之后根據模板語言的標簽分割html文件 reader = _TemplateReader(name, template_string) #分割html文件成為一個一個的對象 #執行_parse方法,將html文件分割成_ChunkList對象 self.file = _File(_parse(reader)) #將html內容格式化成字符串表示的函數 self.code = self._generate_python(loader, compress_whitespace) try: #將字符串表示的函數編譯成函數 self.compiled = compile(self.code, self.name, "exec") except: formatted_code = _format_code(self.code).rstrip() logging.error("%s code:\n%s", self.name, formatted_code) raise
class Template(object): 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, } #創建變量環境並執行函數,詳細Demo見上一篇博文 namespace.update(kwargs) exec self.compiled in namespace execute = namespace["_execute"] try: #執行編譯好的字符串格式的函數,獲取嵌套了值的html文件 return execute() except: formatted_code = _format_code(self.code).rstrip() logging.error("%s code:\n%s", self.name, formatted_code) raise
其中涉及的類有:
class _TemplateReader(object): def __init__(self, name, text): self.name = name self.text = text self.line = 0 self.pos = 0 def find(self, needle, start=0, end=None): assert start >= 0, start pos = self.pos start += pos if end is None: index = self.text.find(needle, start) else: end += pos assert end >= start 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 self.line += self.text.count("\n", self.pos, newpos) 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 type(key) is slice: size = len(self) start, stop, step = key.indices(size) if start is None: start = self.pos else: start += self.pos if stop is not None: stop += self.pos return self.text[slice(start, stop, step)] elif key < 0: return self.text[key] else: return self.text[self.pos + key] def __str__(self): return self.text[self.pos:]
class _ChunkList(_Node): def __init__(self, chunks): self.chunks = chunks def generate(self, writer): for chunk in self.chunks: chunk.generate(writer) def each_child(self): return self.chunks
def _parse(reader, in_block=None): #默認創建一個內容為空列表的_ChunkList對象 body = _ChunkList([]) # 將html塊添加到 body.chunks 列表中 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) line = reader.line # Expression if start_brace == "{{": end = reader.find("}}") if end == -1 or reader.find("\n", 0, end) != -1: raise ParseError("Missing end expression }} on line %d" % line) contents = reader.consume(end).strip() reader.consume(2) if not contents: raise ParseError("Empty expression on line %d" % line) body.chunks.append(_Expression(contents)) continue # Block assert start_brace == "{%", start_brace end = reader.find("%}") if end == -1 or reader.find("\n", 0, end) != -1: raise ParseError("Missing end block %%} on line %d" % line) contents = reader.consume(end).strip() reader.consume(2) if not contents: raise ParseError("Empty block tag ({%% %%}) on line %d" % line) operator, space, suffix = contents.partition(" ") suffix = suffix.strip() # Intermediate ("else", "elif", etc) blocks intermediate_blocks = { "else": set(["if", "for", "while"]), "elif": set(["if"]), "except": set(["try"]), "finally": set(["try"]), } allowed_parents = intermediate_blocks.get(operator) if allowed_parents is not None: if not in_block: raise ParseError("%s outside %s block" % (operator, allowed_parents)) if in_block not in allowed_parents: raise ParseError("%s block cannot be attached to %s block" % (operator, in_block)) body.chunks.append(_IntermediateControlBlock(contents)) continue # End tag elif operator == "end": if not in_block: raise ParseError("Extra {%% end %%} block on line %d" % line) return body elif operator in ("extends", "include", "set", "import", "from", "comment"): if operator == "comment": continue if operator == "extends": suffix = suffix.strip('"').strip("'") if not suffix: raise ParseError("extends missing file path on line %d" % line) block = _ExtendsBlock(suffix) elif operator in ("import", "from"): if not suffix: raise ParseError("import missing statement on line %d" % line) block = _Statement(contents) elif operator == "include": suffix = suffix.strip('"').strip("'") if not suffix: raise ParseError("include missing file path on line %d" % line) block = _IncludeBlock(suffix, reader) elif operator == "set": if not suffix: raise ParseError("set missing statement on line %d" % line) block = _Statement(suffix) body.chunks.append(block) continue elif operator in ("apply", "block", "try", "if", "for", "while"): # parse inner body recursively block_body = _parse(reader, operator) if operator == "apply": if not suffix: raise ParseError("apply missing method name on line %d" % line) block = _ApplyBlock(suffix, block_body) elif operator == "block": if not suffix: raise ParseError("block missing name on line %d" % line) block = _NamedBlock(suffix, block_body) else: block = _ControlBlock(contents, block_body) body.chunks.append(block) continue else: raise ParseError("unknown operator: %r" % operator)
class Template(object): def _generate_python(self, loader, compress_whitespace): buffer = cStringIO.StringIO() try: named_blocks = {} ancestors = self._get_ancestors(loader) ancestors.reverse() for ancestor in ancestors: ancestor.find_named_blocks(loader, named_blocks) self.file.find_named_blocks(loader, named_blocks) writer = _CodeWriter(buffer, named_blocks, loader, self, compress_whitespace) ancestors[0].generate(writer) return buffer.getvalue() finally: buffer.close()
so,上述整個過程其實就是將一個html轉換成一個函數,並為該函數提供全局變量,然后執行該函數!!

結束語
上述就是對於模板語言的整個流程,其本質就是處理html文件內容將html文件內容轉換成函數,然后為該函數提供全局變量環境(即:我們想要嵌套進html中的值和框架自帶的值),再之后執行該函數從而獲取到處理后的結果,再再之后則執行UI_Modules繼續豐富返回結果,例如:添加js文件、添加js內容塊、添加css文件、添加css內容塊、在body內容第一行插入數據、在body內容最后一樣插入數據,最終,通過soekct客戶端對象將處理之后的返回結果(字符串)響應給請求用戶。
