在小公司待久了感覺自己的知識面很小,最近逛博客園和一些技術網站看大家在說JavaScript模版引擎的事兒,完全沒有概念,網上一搜這是08年開始流行起來的。。。本來以為這是很高深的知識,后來在網上看到jQuery作者John Resig,研究了一下,算是明白了最簡單的javaScript模版引擎的原理,並沒有想象的那么高大上,寫篇博客推導一下John Resig寫法的過程,寫出一個最簡單的JavaScript模版引擎。
什么是JavaScript引擎
其實在網站開發中模板還是很常見的一種技術,比如PHP的Smarty、ASP.NET的Master Page等,但這些模板都是基於服務器的,JavaScript模板引擎是為了解決我們在前端寫出形如這樣的拼html的語句
var html='<ul>'; for(var i=0;i<users.length;i++){ html+='<li><a href=">'+users[i].url+'">'+users[i].name+'</a>'; } html+='</ul>'; document.getElementById('results').innerHTML=html;
上面的代碼我們一看就知道是在拼html,但具體拼的什么很難說清,需要逐句去讀代碼,如果我們有這樣一個模板
<ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </ul>
看了很容易就明白開發者希望得到的是這樣的html
<ul> <li><a href="XXX">OOO</a></li> <li><a href="XXX">OOO</a></li> <li><a href="XXX">OOO</a></li> </ul>
JavaScript模板引擎就是幫我們把帶有JavaScript代碼的偽html語句翻譯為html的東東
John Resig的實現方式
先看看John Resig是怎么實現最簡單的一個JavaScript模板引擎的
1 // Simple JavaScript Templating 2 // John Resig - http://ejohn.org/ - MIT Licensed 3 (function(){ 4 var cache = {}; 5 6 this.tmpl = function tmpl(str, data){ 7 // Figure out if we're getting a template, or if we need to 8 // load the template - and be sure to cache the result. 9 var fn = !/\W/.test(str) ? 10 cache[str] = cache[str] || 11 tmpl(document.getElementById(str).innerHTML) : 12 13 // Generate a reusable function that will serve as a template 14 // generator (and which will be cached). 15 new Function("obj", 16 "var p=[],print=function(){p.push.apply(p,arguments);};" + 17 18 // Introduce the data as local variables using with(){} 19 "with(obj){p.push('" + 20 21 // Convert the template into pure JavaScript 22 str 23 .replace(/[\r\t\n]/g, " ") 24 .split("<%").join("\t") 25 .replace(/((^|%>)[^\t]*)'/g, "$1\r") 26 .replace(/\t=(.*?)%>/g, "',$1,'") 27 .split("\t").join("');") 28 .split("%>").join("p.push('") 29 .split("\r").join("\\'") 30 + "');}return p.join('');"); 31 32 // Provide some basic currying to the user 33 return data ? fn( data ) : fn; 34 }; 35 })();
看完上面代碼就明白的同學就不用看下面內容了,沒太明白的同學可以和我一塊兒看看着三十多句代碼為什么能夠實現一個JavaScript引擎吧。
模板的語法
模板的語法很簡單,有三條基本規則
- 用正常的方式書寫html
- 用<% %>嵌套JavaScript語句
- 用<%= %>嵌套JavaScript 變量值
模板轉換為html字符串原理
我們的JavaScript引擎正式設計為識別這種類型的模板的,拿上面的做例子,這樣的一個模版
<ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </ul>
想得到預期html字符串,我們必須設法讓模板內部的javascript變量置換、javaScript語句執行,也就是把JavaScript代碼剝離出來執行,把其它html語句拼接為一個字符串
var p=[]; p.push('<ul>'); for(var i=0;i<users.length;i++){ //javascript語句執行 p.push('<li><a href="'); //html語句拼接
p.push(users[i].url); //javascript變量置換后拼接 p.push('">'); p.push(users[i].name); p.push('</a></li>'); } p.push('</ul>');
最后得到的數組join一下就是我們希望得到的字符串了,首先需要取到模板內的字符串,這個簡單按照John的做法我們可以把模板放到一個script標簽里(防止在頁面顯示出來),換成我們特定的類型
<script type="text/html" id="user_tmpl"> <ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li> <a href="<%=users[i].url%>"> <%=users[i].name%> </a> </li> <% } %> </ul> </script>
這樣就可以通過 document.getElementById(str).innerHTML 來獲取模版內字符串了,然后我們應用一些簡單的法則處理一下模板內字符串
<%=xxx%> => ');p.push(xxx);p.push(' <% => '); %> => p.push('
這樣我們就可以得到這樣的結構,看起來就已經很接近結果了
p.push('<ul>'); for(var i=0;i<users.length;i++){ p.push('<li><a href="'); p.push(users[i].url); p.push('">'); p.push(users[i].name); p.push('</a></li>'); } p.push('</ul>');
簡單的字符串置換
現在我們根據上面規則做替換了,這里得使用一些正則表達式和replace函數的知識,不太熟悉的同學可能需要看看 JavaScript 正則表達式上——基本語法 JavaScript正則表達式下——相關方法
1.把<%=xxx%> 替換為 ');p.push(xxx);p.push('
html=html.replace(/<%=(.*?)%>/g,"');p.push(xxx);p.push('");
2.把<%替換為 ');
html=html.replace(/<%/g,"');");
3.把%> 替換為 p.push('
html=html.replace(/%>/g,"p.push('");
我們再把結果用p.push(' 和 '); 包裹起來就可以看到初步效果了
function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[]; p.push('" +html.replace(/<%=(.*?)%>/g,"');p.push(xxx);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +" ');"; }
這樣我們就把html模版內容替換成了這樣的一個字符串
var result="
var p=[]; p.push('<ul>'); for(var i=0;i<users.length;i++){ p.push('<li><a href="'); p.push(users[i].url); p.push('">'); p.push(users[i].name); p.push('</a></li>'); } p.push('</ul>');"
貌似得到結果了,但我們得到的是字符串,我們預期的是這個字符串執行的結果,很多同學會想到使用eval就可以讓字符串變成JavaScript語句執行,但是Jonh使用了另外一種方式——創建function,我們知道除了常用使用function關鍵字創建一個function
function fn(data){ console.log(data); }
還可以使用Function構造函數來創建一個function
var fn = new Function(arg1, arg2, ..., argN, function_body)
在上面的形式中,每個 arg 都是一個參數,最后一個參數是函數主體(要執行的代碼),使用這種方式可以動態(方法體是動態生成的,提前不知道,當然這樣做會有效率問題)創建一個方法,也就是說我們還可以使用剛才拼出來的javascript字符串動態創建一個函數
function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[];p.push('" +html.replace(/[\r\n\t]/g," ") .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +"');return p.join('');"; var fn=new Function(data,result); return fn(data); }
這樣看起來很科學了,但是我們執行一下會報錯,原因很簡單就是參數的作用域不對,我們需要改變一下動態構造的方法的作用域,這個有很多方式比如apply函數啊什么的,我們暫且采用John的方式——使用with關鍵字改變作用域
function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[];with(obj){p.push('" +html.replace(/[\r\n\t]/g," ") .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +"');}return p.join('');"; var fn=new Function("obj",result); return fn(data); }
雖然看起來和John的方法還有很大區別,不過我們已經偷師到了其精髓,實現了一個最簡單JavaScript模版引擎,你是不是也明白了JavaScript模版引擎是什么了呢?就是簡單的字符串替換,剝離出JavaScript語句,然后利用新的字符串構造函數,返回結果。
看個例子
<!DOCTYPE html> <html> <head> <title>Template</title> </head> <body> <div id="results"></div> <script type="text/html" id="user_tmpl"> <ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </ul> </script> <script type="text/javascript"> var results = document.getElementById("results"); var users=[ {"name":"Byron", "url":"http://localhost"}, {"name":"Casper", "url":"http://localhost"}, {"name":"Frank", "url":"http://localhost"} ]; function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[];with(obj){p.push('" +html.replace(/[\r\n\t]/g," ") .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +"');}return p.join('');"; var fn=new Function("obj",result); return fn(data); } results.innerHTML = tmpl("user_tmpl", users); </script> </body> </html>
應用了簡單的JavaScript模版引擎,我們可以很方便的拼出一些html了
參考
John Resig JavaScript Micro-Templating湯姆大叔 大叔手記(7):構建自己的JavaScript模板小引擎 BarretLee JavaScript模板引擎原理,幾行代碼的事兒 PS. 目前寫的JavaScript模版引擎還有很多性能、特殊情況處理問題,下篇博客(簡單JavaScript模版引擎優化)會繼續進行一些優化工作