由於各種原因,被逼使用前台模板。看了一下其他JS模板庫的實現,發現其原理並不難,遂決定重造輪子。
做一個前台模板,有如下幾個問題需要考量:
- 模板是放置於哪裡?是內嵌於HTML頁面還是像JS檔案那樣獨立出來?如果是內嵌可以減少請求數但無法讓模板重用於另一個HTML頁面,反之亦然。
- 如果是內嵌於HTML頁面,如何存放它?目前有兩種方式,script標籤與textarea。
- 模板界定符的風格,是ASP的<%與%>,還是Django的{{與}},還是其他方式。
下面是我一些不成熟的見解:
- 應該存在兩種模板,普通模板(內嵌於HTML)與局部模板(存放於獨立的檔案中)。普通模板是為某個頁面訂製的,局部模板可以是訂製的,但多是為了實現多頁面的重用,它可以與普通模板一起組成完整的模板。用rails的術語來說,這是一個partial。
- 普通模板的容器為一個喪失解析指令碼能力的script標籤,因為我們可以不需要在script標籤裡面內嵌script標籤。但是與textara作為容器,就很可能碰到模板存在textarea的情況,這時就存在一個錯位套嵌的問題。
- 模板風格,讓它可以定製就行了。預設是ASP風格,好讓它在一些主流的IDE中自動排版。
我把我的模板引擎稱之為ejs(embedded javascript snippet,嵌入式javascript代碼片斷)。任何javascript模板只最終目的就是產生一個可以傳入後台參數的函數。
<!doctype html><html> <head> <meta charset="utf-8"/> <meta content="IE=8" http-equiv="X-UA-Compatible"/> <meta name="keywords" content="javascript模板 by 司徒正美" /> <meta name="description" content="javascript模板 by 司徒正美" /> <title>javascript模板 by 司徒正美</title> </head> <body> <h1>javascript模板 by 司徒正美</h1> <script id="tmpl" type="text/html"> <h2><%= name %></h2> <%# 這是注釋!!!!!!!!! %> <ul> <% for(var i=0; i< supplies.length; i++){ %> <li><%= supplies[i] %></li> <% } %> </ul> <% var color = "color:red;" %> <p style="text-indent:2em;<%= color %>"><%= address %></p> </script> <script id="tmpl" type="template" ></script > <script> window.onload = function(){ dom.ejs({ selector:"tmpl", json: { name:"司徒正美", supplies:["第一個LI元素","第二個LI元素","第三個LI元素","第四個LI元素"], address:"異次元"} }); } </script> </body></html>
模板系統:
//司徒正美 javascript template - http://www.cnblogs.com/rubylouvre/ - MIT Licensed (function () { if(!String.prototype.trim){ String.prototype.trim = function(str) { return this.replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); } } var dom = { quote: function (str) { str = str.replace(/[\x00-\x1f\\]/g, function (chr) { var special = metaObject[chr]; return special ? special : '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).slice(-4) }); return '"' + str.replace(/"/g, '\\"') + '"'; } }, metaObject = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\\': '\\\\' }, parser = document.createElement("div"), startOfHTML = "\t__views.push(", endOfHTML = ");\n", outerScan = function(str,buff,left,right){ var index = str.indexOf(left); if(index !== -1){ buff.push(startOfHTML, dom.quote(str.slice(0,index)), endOfHTML); innerScan(str.slice(index+2),buff,left,right); }else{ buff.push(startOfHTML, dom.quote(str), endOfHTML); } }, innerScan = function(str,buff,left,right){ var index = str.indexOf(right); if(index !== -1){ var text = str.slice(0,index); switch (text.charAt(0)) { case "#"://處理注釋 break; case "="://處理後台返回的變數(輸出到頁面的) buff.push(startOfHTML, text.slice(1), endOfHTML) break; default: buff.push(text, "\n") } outerScan( str.slice(index+2),buff,left,right); }else{ throw "找不到右界定符 " + str } } //onsite,可選,Boolean,是否就地替換掉模板容器,預設true,如果為false,則返回一個文檔片段,交由使用者自己插入到需要的地方 dom.ejs = function (obj) { var onsite = obj.onsite === void 0 , left = obj.left || "<%", right = obj.right || "%>", selector = obj.selector, buff = ["var __views = [];\n"], fragment = document.createDocumentFragment(), el = document.getElementById(selector), ejs = dom.ejs; if (!el) throw "找不到目標元素"; if(!ejs[selector]){ outerScan(el.text.trim(),buff,left,right); ejs[selector] = new Function("json", "with(json){"+buff.join("") + '};return __views.join("");') } parser.innerHTML = ejs[selector](obj.json || {}); while (parser.firstChild) { fragment.appendChild(parser.firstChild) } return onsite ? el.parentNode.replaceChild(fragment, el) : fragment; }; window.dom = dom; })();
<br /><!doctype html><br /><html><br /> <head><br /> <meta charset="utf-8"/><br /> <meta content="IE=8" http-equiv="X-UA-Compatible"/><br /> <meta name="keywords" content="javascript模板 by 司徒正美" /><br /> <meta name="description" content="javascript模板 by 司徒正美" /><br /> <title>javascript模板 by 司徒正美</title><br /> </head><br /> <body><br /> <h1>javascript模板 by 司徒正美</h1><br /> <script id="tmpl" type="text/html"><br /> <h2><%= name %></h2><br /> <%# 這是注釋!!!!!!!!! %><br /> <ul><br /> <% for(var i=0; i< supplies.length; i++){ %><br /> <li><%= supplies[i] %></li><br /> <% } %><br /> </ul><br /> <% var color = "color:red;" %><br /> <p style="text-indent:2em;<%= color %>"><%= address %></p><br /> </script></p><p> <script></p><p> (function () {<br /> if(!String.prototype.trim){<br /> String.prototype.trim = function(str) {<br /> return this.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');<br /> }<br /> }<br /> var dom = {<br /> quote: function (str) {<br /> str = str.replace(/[\x00-\x1f\\]/g, function (chr) {<br /> var special = metaObject[chr];<br /> return special ? special : '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).slice(-4)<br /> });<br /> return '"' + str.replace(/"/g, '\\"') + '"';<br /> }<br /> },<br /> metaObject = {<br /> '\b': '\\b',<br /> '\t': '\\t',<br /> '\n': '\\n',<br /> '\f': '\\f',<br /> '\r': '\\r',<br /> '\\': '\\\\'<br /> },<br /> parser = document.createElement("div"),<br /> startOfHTML = "\t__views.push(",<br /> endOfHTML = ");\n",<br /> outerScan = function(str,buff,left,right){<br /> var index = str.indexOf(left);<br /> if(index !== -1){<br /> buff.push(startOfHTML, dom.quote(str.slice(0,index)), endOfHTML);<br /> innerScan(str.slice(index+2),buff,left,right);<br /> }else{<br /> buff.push(startOfHTML, dom.quote(str), endOfHTML);<br /> }<br /> },<br /> innerScan = function(str,buff,left,right){<br /> var index = str.indexOf(right);<br /> if(index !== -1){<br /> var text = str.slice(0,index);<br /> switch (text.charAt(0)) {<br /> case "#"://處理注釋<br /> break;<br /> case "="://處理後台返回的變數(輸出到頁面的)<br /> buff.push(startOfHTML, text.slice(1), endOfHTML)<br /> break;<br /> default:<br /> buff.push(text, "\n")<br /> }<br /> outerScan( str.slice(index+2),buff,left,right);<br /> }else{<br /> throw "找不到右界定符 " + str<br /> }<br /> }</p><p> //onsite,可選,Boolean,是否就地替換掉模板容器,預設true,如果為false,則返回一個文檔片段,交由使用者自己插入到需要的地方<br /> dom.ejs = function (obj) {<br /> var onsite = obj.onsite === void 0 ,<br /> left = obj.left || "<%",<br /> right = obj.right || "%>",<br /> selector = obj.selector,<br /> buff = ["var __views = [];\n"],<br /> fragment = document.createDocumentFragment(),<br /> el = document.getElementById(selector),<br /> ejs = dom.ejs;<br /> if (!el) throw "找不到目標元素";<br /> if(!ejs[selector]){<br /> outerScan(el.text.trim(),buff,left,right);<br /> ejs[selector] = new Function("json", "with(json){"+buff.join("") + '};return __views.join("");')<br /> }<br /> parser.innerHTML = ejs[selector](obj.json || {});<br /> while (parser.firstChild) {<br /> fragment.appendChild(parser.firstChild)<br /> }<br /> return onsite ? el.parentNode.replaceChild(fragment, el) : fragment;<br /> };<br /> window.dom = dom;</p><p> })();</p><p> window.onload = function(){<br /> dom.ejs({<br /> selector:"tmpl",<br /> json: {<br /> name:"司徒正美",<br /> supplies:["第一個LI元素","第二個LI元素","第三個LI元素","第四個LI元素"],<br /> address:"異次元"}<br /> });<br /> }<br /> </script><br /> </body><br /></html><br />
運行代碼
PS:發現javascript模板沒有想象中的糟糕,尤其是大流量的頁面在無法使用UI庫的情況下,這是個不錯的選擇,例子如qq zone與ニコニコ動畫。