翻譯_只需20行代碼創造JavaScript模板引擎(二)

上文連接翻譯_只需20行代碼創造JavaScript模板引擎(一)

可是這還不夠好,數據是很是簡單的對象,而且很容易使用object['property']對象的中括號語法,去讀取對象的值。css

但在實踐中,咱們用到的數據中,可能有複雜的嵌套對象。html

//嵌套對象
data = {
    name: "Krasimir Tsonev",
    profile: {age:29}
}

若是有複雜的嵌套對象,就不能用對象的中括號語法讀取值了。正則表達式

因此String.prototype.replace(matchedStr, data["profile.age"]) 就行不通了。segmentfault

由於data["profile.age"],每次返回undefined。數組

//對象的中括號語法讀取值 object["property"]

var obj = {
    name: "Shaw",
    age: 18
}

console.log(obj["name"]); //"Shaw"
console.log(obj["age"]); // 18  


//複雜的嵌套對象,就不能用對象的中括號語法讀取值了。

var obj = {
    name: "Shaw",
    profile: {
        age: 18
    }
}

console.log(obj["profile.age"]); // undefined

那麼,怎麼解決這個問題?
最好的辦法是在模板中<%和%>之間放置真正的JavaScript代碼。app

var tpl = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';

這怎麼可能呢? John使用了new Function()語法, 沒有顯式的聲明函數。函數

var fn = new Function("arg", "console.log(arg+1);");
fn(2); // 3

//fn是一個能夠傳入一個參數的函數
//fn函數體內的語句,就是 console.log(arg+1);

/* 等價於*/

function fn(arg) {
    console.log(arg +1);
}

fn(2); //3

咱們能夠利用這個語法,在一行代碼中,定義函數、參數和函數體。這正是咱們須要的。優化

在利用這種語法,建立函數以前。this

咱們必須設計好,函數體怎麼寫。函數執行後應該返回最終編譯的模板。prototype

回想一下,咱們常常使用的字符竄拼接方法。

"<p>Hello, my name is " +
this.name +
". I\'m" +
this.profile.age +
" years old.</p>";

這說明,咱們能夠把字符竄模板裏面的內容拆解,拆解爲html和JavaScript代碼。

通常狀況下,咱們都是使用for循環去遍歷數據。

// 傳入的字符竄模塊
var template =
'My Skill:' +
'<%for(var index in this.skills) {%>' +
'<a href=""><%this.skills[index]%></a>' +
'<%}%>';
// 預想的返回結果

return 
'My skills:' +
for(var index in this.skills) { +
'<a href="">' +
this.skills[index] +
'</a>' +
}

固然,這將產生一個錯誤。這就是爲何我決定遵循約翰文章中使用的邏輯,把全部的字符串放到一個數組中。

var r = [];
r.push('My skills:');
for(var index in this.skills) {
    r.push('<a href="">');
    r.push(this.skills[index]);
    r.push('</a>');
}
return r.join('');

下一個邏輯步驟是收集定製生成函數的不一樣行。
咱們已經從模板中提取了一些信息。咱們知道佔位符的內容和它們的位置。因此,經過使用一個輔助變量(遊標)

function TemplateEngine(tpl, data) {

    var tplExtractPattern = /<%([^%>]+)?%>/g,
        code = 'var r=[];\n',
        cursor = 0,
        match;

    function addCode(line) {
        code += 'r.push("' + line.replace(/"/g,'\\"') +'"); \n';
    }

    while(match = tplExtractPattern.exec(tpl)) {
        addCode(tpl.slice(cursor, match.index));
        addCode(match[1]);
        cursor = match.index + match[0].length;
    }

    code += 'return r.join("");';

    console.log(code);

    return tpl;

}

var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';

TemplateEngine(template, {
    name: "Shaw",
    profile: { age: 18 }
});

變量code保存着函數體的代碼。
在while循環語句中,咱們也須要變量cursor遊標,告訴咱們字符竄slice()方法截取的起始座標和末尾座標。
變量code在while循環語句中,不斷的拼接。

可是code的最終結果是

/*
var r=[];
r.push("<p>Hello, my name is "); 
r.push("this.name"); 
r.push(". I'm "); 
r.push("this.profile.age"); 
return r.join("");
<p>Hello, my name is <%this.name%>. I'm <%this.profile.age%> years old.</p>
*/

這不是咱們想要的。 "this.name" 和 "this.profile.name" 不該該被引號包裹。

因此咱們須要addCode函數作一個小小的改動

function TemplateEngine(tpl,data){

    var tplExtractPattern = /<%([^%>]+)?%>/g,
        code = 'var r=[];\n',
        cursor = 0,
        match;

    function addCode(line, js) {
        if(js) {
            code += 'r.push(' + line + ');\n';
        } else {
            code += 'r.push("' + line.replace(/"/g,'\\"') + '");\n';
        }
    }

    while(match = tplExtractPattern.exec(tpl)) {
        addCode(tpl.slice(cursor, match.index));
        addCode(match[1], true);
        cursor = match.index + match[0].length;
    }

    code += 'return r.join("");';

    console.log(code);

    return tpl;

}

var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';

TemplateEngine(template, {
    name: "Shaw",
    profile: { age: 18 }
});

如今this能夠正確指向執行對象了。

var r=[];
r.push("<p>Hello, my name is ");
r.push(this.name);
r.push(". I'm ");
r.push(this.profile.age);
return r.join("");

到了這裏,咱們只須要建立函數並執行它。
在TemplateEngine函數裏把return tpl 替換成

return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);

咱們不須要傳入參數,這裏我使用apply()方法,改變了做用域,如今this.name指向了data。

幾乎已經完成了。可是咱們還須要支持更多JavaScript關鍵字,好比if/else,循環流程語句。
讓咱們從相同的例子,再次進行構思。

function TemplateEngine(tpl,data){

    var tplExtractPattern = /<%([^%>]+)?%>/g,
        code = 'var r=[];\n',
        cursor = 0,
        match;

    function addCode(line, js) {
        if(js) {
            code += 'r.push(' + line + ');\n';
        } else {
            code += 'r.push("' + line.replace(/"/g,'\\"') + '");\n';
        }
    }

    while(match = tplExtractPattern.exec(tpl)) {
        addCode(tpl.slice(cursor, match.index));
        addCode(match[1], true);
        cursor = match.index + match[0].length;
    }

    code += 'return r.join("");';

    console.log(code);

    return new Function(code.replace(/[\t\n\t]/g, '')).apply(data);
}

var template = 
'My skill:' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';

TemplateEngine(template, {
    skills: ["js", "html", "css"]
}); 
// Uncaught SyntaxError: Unexpected token for

調用TemplateEngine(),控制檯報錯了,Uncaught SyntaxError: Unexpected token for。
在控制檯打印出,拼接的代碼

var r=[];
r.push("My skill:");
r.push(for(var index in this.skills) {);
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
r.push(});
return r.join("");

帶有for循環的語句不該該被直接放到數組裏面,而是應該做爲腳本的一部分直接運行。因此在把代碼語句添加到code變量以前還要多作一個判斷。

var re = /<%([^%>]+)?%>/g,
    reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
    code = 'var Arr = [];\n',
    cursor = 0;

function addCode(line,js) {
    if(js){
        if(line.match(reExp)) {
            code += line +'\n';
        } else {
            code += 'r.push(' + line + ');\n';
        }
    } else {
        code += 'r.push("' + line.replace(/"/g,'\\"')) + '");\n';
    }
}

添加一個新的正則表達式。它會判斷代碼中是否包含if、for、else等關鍵字。
若是有的話就直接添加到腳本代碼中去,不然就添加到數組中去。

function TemplateEngine(tpl,data){

    var tplExtractPattern = /<%([^%>]+)?%>/g,
    jsExtractReExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
    code = 'var arr = [];\n',
    cursor = 0,
    match;

    function addCode(line,js) {
        if(js){
            if(line.match(jsExtractReExp)) {
                code += line +'\n';
            } else {
                code += 'arr.push(' + line + ');\n';
            }
        } else {
            code += 'arr.push("' + line.replace(/"/g,'\\"') + '");\n';
        }
    }


    while(match = tplExtractPattern.exec(tpl)) {
        addCode(tpl.slice(cursor, match.index));
        addCode(match[1], true);
        cursor = match.index + match[0].length;
    }

    code += 'return arr.join("");';

    console.log(code);

    return new Function(code.replace(/[\t\n\t]/g, '')).apply(data);
}

var template = 
'My skill:' +
'<%for(var index in this.skills) {%>' +
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';

TemplateEngine(template, {
    skills: ["js", "html", "css"]
});


/*
var arr = [];
arr.push("My skill:");
for(var index in this.skills) {
arr.push("<a href=\"#\">");
arr.push(this.skills[index]);
arr.push("</a>");
}
return arr.join("");
*/

//"My skill:<a href="#">js</a><a href="#">html</a><a href="#">css</a>"

一切都是正常編譯的 :)。

最後的修改,實際上給了咱們更強大的處理能力。

咱們能夠直接將複雜的邏輯應用到模板中。例如

var template = 
'My skills:' + 
'<%if(this.showSkills) {%>' +
    '<%for(var index in this.skills) {%>' + 
    '<a href="#"><%this.skills[index]%></a>' +
    '<%}%>' +
'<%} else {%>' +
    '<p>none</p>' +
'<%}%>';
console.log(TemplateEngine(template, {
    skills: ["js", "html", "css"],
    showSkills: true
}));

/*
var arr = [];
arr.push("My skills:");
if(this.showSkills) {
arr.push("");
for(var index in this.skills) {
arr.push("<a href=\"#\">");
arr.push(this.skills[index]);
arr.push("</a>");
}
arr.push("");
} else {
arr.push("<p>none</p>");
}
return arr.join("");
*/

//"My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>"

最後,我進一步作了一些優化,最終版本以下

var TemplateEngine = function(templateStr, data) {
    var tplStrExtractPattern = /<%([^%>]+)?%>/g,
        jsKeyWordsExtractPattern = /(^( )?(for|if|else|swich|case|break|{|}))(.*)?/g,
        code = 'var arr = [];\n',
        cursor = 0,
        match;
    var addCode = function(templateStr, jsCode) {
        if(jsCode) {
            if(templateStr.match(jsKeyWordsExtractPattern)) {
                code += templateStr + '\n';
            } else {
                code += 'arr.push(' + templateStr + ');\n';
            }
        } else {
            code += 'arr.push("' + templateStr.replace(/"/g, '\\"') + '");\n';
        }
    }
    while(match = tplStrExtractPattern.exec(templateStr)){
        addCode(templateStr.slice(cursor, match.index));
        addCode(match[1], true);
        cursor = match.index + match[0].length;
    }
    code += 'return arr.join("");';
    return new Function(code.replace(/[\r\n\t]/g, '')).apply(data);
}
相關文章
相關標籤/搜索