概念:
模板引擎(這里特指用于Web開(kāi)發(fā)的模板引擎)是為了使用戶界面與業(yè)務(wù)數(shù)據(jù)(內(nèi)容)分離而產(chǎn)生的,它可以生成特定格式的文檔,用于網(wǎng)站的模板引擎就會(huì)生成一個(gè)標(biāo)準(zhǔn)的HTML文檔
使用原因:
前端模板引擎出現(xiàn)之前,在需要用js渲染頁(yè)面的情況下,我們一般會(huì)通過(guò)js將數(shù)據(jù)和生成結(jié)構(gòu)綁定。例如:
var html = "";
for(var i =0;i<data.length;i++){
html+="<li>"+data[i]+"</li>"
}
一旦業(yè)務(wù)復(fù)雜起來(lái),或者多人維護(hù)的情況下,幾乎會(huì)失控。而且如果需要拼接的 HTML 代碼里有很多引號(hào)的話(比如有大量的 href 屬性,src 屬性),那么就非常容易出錯(cuò)了。
動(dòng)手寫(xiě)一個(gè)引擎:
參考http://blog.jobbole.com/56689/;
一:從簡(jiǎn)單的說(shuō)起(字符串的替換):
比如:
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
data = { name: "Krasimir", age: 29 }
最終轉(zhuǎn)換為
<p>Hello, my name is Krasimir . I\'m 29 years old.</p>
步驟
1.使用正則查找所有<%屬性名%>片段
var re = /<%([^%>]+)?%>/g;//
這句正則表達(dá)式會(huì)捕獲所有以<%開(kāi)頭,以%>結(jié)尾的片段;
[^%>] :^表示非,意為片段中不包含%或者不包含>的部分;
()為捕獲組;
2.將每次得到片段替換為數(shù)據(jù)中的屬性值;
這里使用exec();取得
[0:"<%name%>"http://與正則表達(dá)式相匹配的文本
1:"name"http://捕獲組里面的內(nèi)容
index:21//匹配文本的每次起始位置
input:"<p>Hello, my name is frank. I'm <%age%> years old.</p>"http://被檢索的字符串 string
]
不確定次數(shù)的循環(huán)操作使用while()
while(match = regex.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]])
//match[0 ] :循環(huán)第一次得出<%name%> ;循環(huán)第二次得出<%age%>
//data[match[1]:循環(huán)第一次得出 data['name'] ; 循環(huán)第二次得出data['age']
}
整體代碼如下:
var TemplateEngine = function(tpl, data) {
var regex = /<%([^%>]+)?%>/g;
while(match = regex.exec(tpl)) {
tpl = tpl.replace(match[0], data[match[1]])
}
return tpl;
}
TemplateEngine(template, data)
二:通過(guò)new Function();將字符轉(zhuǎn)換為js代碼
上面的例子我們以data[“property”]的方式使用了一個(gè)簡(jiǎn)單對(duì)象來(lái)傳遞數(shù)據(jù),但是實(shí)際情況下我們很可能需要更復(fù)雜的嵌套對(duì)象,如果將data改成如下。
eg:data={
name: "Krasimir Tsonev",
profile: { age: 29 }
}
在模板中使用
<%profile.age%>的話,代碼會(huì)被替換成data[‘profile.age’],結(jié)果是undefined。這樣我們就不能簡(jiǎn)單地用replace函數(shù),而是要用別的方法。如果能夠在<%和%>之間直接使用Javascript代碼就最好了。
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
最終將template轉(zhuǎn)換成js代碼為
"<p>Hello, my name is "
+ this.name+
". I\'m "
+this.profile.age+
" years old.</p>"
//this.屬性名:為這個(gè)方法作用域下數(shù)據(jù)的屬性值;
//****注:代碼特點(diǎn)為,字符串加引號(hào),非字符即代碼運(yùn)行部分沒(méi)有引號(hào)?。?!****
步驟
一、準(zhǔn)備前提:new Function("字符串"):
var fun = new Function("a","console.log(a)"); //可以執(zhí)行字符串內(nèi)的代碼
fun(1)//1;
此代碼相當(dāng)于
var fun = function(a){
console.log(a)
}
fun(1)
由此將上述模板轉(zhuǎn)為
var TemplateEngine= function(data){
return "<p>Hello, my name is "+ this.name+ ". I\'m " +this.profile.age+" years old.</p>"
}
var data={
name: "Krasimir Tsonev",
profile: { age: 29 }
}
TemplateEngine.apply(data)//在data的作用域下使用方法TemplateEngine;
最終轉(zhuǎn)為new Function("字符串")的寫(xiě)法為;
var template= "<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>"
var result = new Function(template).apply(data);
二、正式開(kāi)始:模板用正則替換為js代碼:
var template= '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>'
希望得到模板的js格式為
"<p>Hello, my name is " //字符
+ this.name+//代碼
". I\'m "
+this.profile.age+
" years old.</p>"
js格式實(shí)現(xiàn)之前的字符效果為(后期通過(guò)new Function()轉(zhuǎn)成js代碼)
var arr=[];
arr.push("<p>Hello, my name is ");
arr.push(this.name);
arr.push(". I'm ");
arr.push(this.profile.age);
arr.push(" years old.</p>");
return arr.join("")
思路:需要在上述js格式的每行打個(gè)隔斷,即碰到<%...%>,做個(gè)隔斷,同時(shí)判斷若為字符串加引號(hào),,若為js代碼直接輸出,以下代碼用變量code接住所有隔斷,用方法add()實(shí)現(xiàn)判斷js并推入隔斷。與此同時(shí)需要考慮模板中的引號(hào)需要轉(zhuǎn)義,以免影響js效果。
整體代碼如下:
function TemplateEngine(tpl,data) {
var reg = /<%([^%>]+)?%>/g;
var code = 'var arr=[];\n',
cur=0; //游標(biāo),用來(lái)記錄查找的位置;
while(match = reg.exec(tpl)){
add(tpl.slice(cur,match['index'])); //eg:第一次循環(huán)加入arr.push("<p>Hello, my name is ");
add(match[1],true);//eg:第一次循環(huán)加入arr.push(this.name);
cur=match['index']+match[0].length; // 記錄下一次循環(huán),需要截取的起始位置
}
//至此截取到最后一個(gè)<%this.profile.age%>
add(tpl.slice(cur)); // 截取剩余模板剩余部分
code+="return arr.join(\"\")"; // 由于code在new Function()中,因此需要返回運(yùn)行代碼。
function add(line ,js) {
js?code+="arr.push("+line+");\n":
code+="arr.push(\""+line.replace('\"','\\"')+"\");\n";
}
return new Function(code).apply(data);
}
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
var data = {
name: "lili",
profile: { age: 30 }
};
console.log(TemplateEngine(template, data));
模板引擎接近完成了,不過(guò)還有一點(diǎn),我們需要支持更多復(fù)雜的語(yǔ)句,比如條件判斷和循環(huán)。我們接著上面的例子繼續(xù)寫(xiě)。
var template =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';
console.log(TemplateEngine(template, {
skills: ["js", "html", "css"]
}));
沿用上一個(gè)方法,這里會(huì)產(chǎn)生一個(gè)異常,Uncaught SyntaxError: Unexpected token for。如果我們調(diào)試一下,把code變量打印出來(lái),我們就能發(fā)現(xiàn)問(wèn)題所在。
var r=[];
r.push("My skills:");
r.push(for(var index in this.skills) {);
r.push("<a href=\"\">");
r.push(this.skills[index]);
r.push("</a>");
r.push(});
r.push("");
return r.join("");
帶有for循環(huán)的那一行不應(yīng)該被直接放到數(shù)組里面,而是應(yīng)該作為腳本的一部分直接運(yùn)行。所以我們?cè)诎褍?nèi)容添加到code變量之前還要多做一個(gè)判斷。
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var r=[];\n',
cursor = 0;
var add = function(line, js) {
js? code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n' :
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
這里我們新增加了一個(gè)正則表達(dá)式。它會(huì)判斷代碼中是否包含if、for、else等等關(guān)鍵字。如果有的話就直接添加到腳本代碼中去,否則就添加到數(shù)組中去。運(yùn)行結(jié)果如下:
var r=[];
r.push("My skills:");
for(var index in this.skills) {
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
}
r.push("");
return r.join("");
最后一個(gè)改進(jìn)可以使我們的模板引擎更為強(qiáng)大。我們可以直接在模板中使用復(fù)雜邏輯,例如:
var songs =[
{name:'剛剛好', singer:'薛之謙', url:'http://music.163.com/xxx'},
{name:'最佳歌手', singer:'許嵩', url:'http://music.163.com/xxx'},
{name:'初學(xué)者', singer:'薛之謙', url:'http://music.163.com/xxx'},
{name:'紳士', singer:'薛之謙', url:'http://music.163.com/xxx'},
{name:'我們', singer:'陳偉霆', url:'http://music.163.com/xxx'},
{name:'畫(huà)風(fēng)', singer:'后弦', url:'http://music.163.com/xxx'},
{name:'We Are One', singer:'郁可唯', url:'http://music.163.com/xxx'}
];
var html =
'<div class="song-list">'+
' <h1>熱歌榜</h1>'+
' <ol>'+
'<%for(var i=0; i<this.songs.length;i++){%>'+
'<li><%this.songs[i].name%> - <%this.songs[i].singer%></li>'+
'<%}%>'+
' </ol>'+
'</div>';
最終版本代碼為
var TemplateEngine = function(html,options) {
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var Arr=[];\n',
cursor = 0;
var add = function(line, js) {
js? (code += line.match(reExp) ? line + '\n' : 'Arr.push(' + line + ');\n') :
(code += line != '' ? 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
};
while(match = re.exec(html)) {
add(html.slice(cursor, match.index));
add(match[1], true);
cursor = match.index + match[0].length;
}
add(html.substr(cursor, html.length - cursor));
code += "console.log(this);\n";
code += 'return Arr.join("");';
return new Function (code.replace(/[\r\t\n]/g, '')).apply(null,songs);//此處需注意變量songs不是對(duì)象,需要將apply(null,songs)加null,變?yōu)槿肿饔糜颍? };
var results =TemplateEngine (html,songs);
document.body.innerHTML = results;