淺談模板引擎原理及制作

概念:

模板引擎(這里特指用于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;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 【HELLO小程序?qū)ёx】:AbsurdJS 作者寫(xiě)的一篇教程,一步步教你怎樣用 Javascript 實(shí)現(xiàn)一個(gè)純客...
    c14328d5898b閱讀 524評(píng)論 0 3
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚(yú)_t_閱讀 34,625評(píng)論 18 399
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評(píng)論 19 139
  • 個(gè)人入門(mén)學(xué)習(xí)用筆記、不過(guò)多作為參考依據(jù)。如有錯(cuò)誤歡迎斧正 目錄 簡(jiǎn)書(shū)好像不支持錨點(diǎn)、復(fù)制搜索(反正也是寫(xiě)給我自己看...
    kirito_song閱讀 2,637評(píng)論 1 37

友情鏈接更多精彩內(nèi)容