上一篇《白話tornado原始碼(3):請求來了》介紹了客戶端請求在tornado框架中的生命週期,其本質就是利用epoll和socket來獲取並處理請求。在上一篇的內容中,我們只是給客戶端返回了簡單的字串,如:“Hello World”,而在實際開發中,需要使用html檔案的內容作為模板,然後將被處理後的資料(計算或資料庫中的資料)巢狀在模板中,然後將巢狀了資料的html檔案的內容返回給請求者客戶端,本篇就來詳細的剖析模板處理的整個過程。
概述
(配圖超大,請點選這裡看大圖)
上圖是返回給使用者一個html檔案的整個流程,較之前的Demo多了綠色流線的步驟,其實就是把【self.write(‘hello world’)】變成了【self.render(‘main.html’)】,對於所有的綠色流線只做了五件事:
- 使用內建的open函式讀取Html檔案中的內容
- 根據模板語言的標籤分割Html檔案的內容,例如:{{}} 或 {%%}
- 將分割後的部分資料塊格式化成特殊的字串(表示式)
- 通過python的內建函式執行字串表示式,即:將html檔案的內容和巢狀的資料整合
- 將資料返回給請求客戶端
所以,如果要返回給客戶端對於一個html檔案來說,根據上述的5個階段其內容的變化過程應該是這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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> XXXHandler.get |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!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 2 3 4 5 6 7 8 |
第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>' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
'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) ' |
1 2 |
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,將處理請求後的資料返回給請求客戶端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#!/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() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> </head> <body> <hr> {% module Foo() %} <hr> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<!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原始碼之褪去模板外衣的前戲 》
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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) |
1 2 3 4 5 6 7 8 9 10 11 12 |
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 = {} Loader.__init__ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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] Loader.load |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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 Template.__init__ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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 Template.generate |
其中涉及的類有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
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:] _TemplateReader |
1 2 3 4 5 6 7 8 9 10 11 12 |
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 _ChunkList |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
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) _parse |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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() Template._generate_python |
so,上述整個過程其實就是將一個html轉換成一個函式,併為該函式提供全域性變數,然後執行該函式!!
結束語
上述就是對於模板語言的整個流程,其本質就是處理html檔案內容將html檔案內容轉換成函式,然後為該函式提供全域性變數環境(即:我們想要巢狀進html中的值和框架自帶的值),再之後執行該函式從而獲取到處理後的結果,再再之後則執行UI_Modules繼續豐富返回結果,例如:新增js檔案、新增js內容塊、新增css檔案、新增css內容塊、在body內容第一行插入資料、在body內容最後一樣插入資料,最終,通過soekct客戶端物件將處理之後的返回結果(字串)響應給請求使用者。