20行代碼實現JavaScript模板引擎

本文首發於我的博客: icyfish.mejavascript

正文

刷朋友圈看到了一個不錯的題目, 因而Google了一下, 找到一篇文章: JavaScript template engine in just 20 lines, 並非逐字逐句翻譯, 所以算是翻譯+筆記吧.css

var TemplateEngine = function(tpl, data) {
    // magic here ...
}
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
console.log(TemplateEngine(template, {
    name: "Krasimir",
    age: 29
}));複製代碼

如今咱們要實現TemplateEngine函數, 由上可知, 該函數的兩個參數爲模板及數據. 執行上述代碼後會出現如下結果: html

<p>Hello, my name is Krasimir. I'm 29 years old.</p>複製代碼

首先咱們必需要獲取模板中的動態變化部分, 以後將用二個參數中的真實數據替換動態變化部分的內容, 可使用正則表達式實現.java

var re = /<%([^%>]+)?%>/g;複製代碼

上面的表達式會提取全部以<%爲開頭, %>爲結尾的部份內容, 末尾的g(global)表示匹配全部項. 而後使用RegExp.prototype.exec()方法, 將全部匹配的字符串存進一個數組中.git

var re = /<%([^%>]+)?%>/g;
var match = re.exec(tpl);複製代碼

輸出match獲得這樣的結果:github

[
    "<%name%>",
    " name ", 
    index: 21,
    input: 
    "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
]複製代碼

咱們提取出了數據, 可是隻獲得一個數組元素, 咱們須要處理的是全部匹配項, 所以使用while循環實現:正則表達式

var re = /<%([^%>]+)?%>/g, match;
while(match = re.exec(tpl)) {
    console.log(match);
}複製代碼

執行上述代碼以後會發現<%name%><%age%>都被提取出來了.數組

接下來要用真實的數據取代佔位符. 最簡單的方式是使用String.prototype.replace()方法實現:app

var TemplateEngine = function(tpl, data) {
    var re = /<%([^%>]+)?%>/g, match;
    while(match = re.exec(tpl)) {
        tpl = tpl.replace(match[0], data[match[1]])
    }
    return tpl;
}複製代碼

對於文章開頭的例子, 由於只是簡單的對象, 使用當前的方式(data["property"])就可以完成任務, 可是實際上會遇到更復雜的多層嵌套對象, 好比:函數

{
    name: "Krasimir Tsonev",
    profile: { age: 29 }
}複製代碼

將函數的第二個參數改爲上述形式以後, 使用以上的方法就沒有辦法解決問題了, 由於當咱們輸入<%profile.age%>時, 獲得的數據是["profile.age"], 其值爲undefined. 此時replace()方法再也不適用. 若是對於在<%%>之間的內容, 將其當作JavaScript代碼, 能夠直接執行並返回值, 那就比較好了, 好比:

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

使用new Function()語法, 構造函數:

var fn = new Function("arg", "console.log(arg + 1);");
fn(2); // outputs 3複製代碼

fn函數接受一個參數, 其函數體爲console.log(arg + 1), 上述的代碼至關於:

var fn = function(arg) {
    console.log(arg + 1);
}
fn(2); // outputs 3複製代碼

如今咱們知道了能夠經過上述方式由字符串構造出一個簡單的函數. 不過在實現咱們的需求時, 還須要花點時間思考如何構建咱們所需的函數體. 該函數的功能是返回編譯後的模板. 開始試試看如何實現:

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

將模板分離爲由文本和JavaScript代碼組成的部分. 利用簡單的合併就能夠得到預期的結果. 不過該方法仍是沒法100%符合咱們的要求. 由於若是<%%>之間的內容不是簡單的變量, 而是其餘更復雜的好比循環語句, 就沒法得到預期結果, 例如:

var template = 
'My skills:' + 
'<%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>' +
}複製代碼

這樣的話會產生錯誤, for(var index in this.skills) {沒法正常執行, 所以採用另外一種方式, 不要將全部內容添加到數組中, 而只將所需的內容添加, 最後合併數組:

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('');複製代碼

所以接下來的步驟是在構造的函數體中根據狀況添加各行代碼, 以前咱們已從模板中提取出一些相關的信息: 佔位符的內容以及它們所處的位置. 那麼, 再定義一個輔助的變量(cursor)就可以實現咱們想要獲得的結果.

var TemplateEngine = function(tpl, data) {
    var re = /<%([^%>]+)?%>/g,
        code = 'var r=[];\n',
        cursor = 0, 
        match;
    var add = function(line) {
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
    }
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1]);
        cursor = match.index + match[0].length;
    }
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");'; // <-- return the result
    console.log(code);
    return tpl;
}
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
console.log(TemplateEngine(template, {
    name: "Krasimir Tsonev",
    profile: { age: 29 }
}));複製代碼

code變量的值爲咱們本身構造的函數的函數體, 函數體中首先定義了一個空數組. 能夠經過cursor變量存儲<%this.name%>這種形式的內容以後的文字處於模板中的位置索引值. 而後咱們又建立了add函數, 利用這個函數能夠添加各行代碼到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("");複製代碼

this.namethis.profile.age不該該被雙引號引發. 能夠這樣改進add函數來解決這個問題:

var add = function(line, js) {
    js? code += 'r.push(' + line + ');\n' :
        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
}
var match;
while(match = re.exec(tpl)) {
    add(tpl.slice(cursor, match.index));
    add(match[1], true); // <-- say that this is actually valid js
    cursor = match.index + match[0].length;
}複製代碼

若是佔位符的內容爲JS代碼, 則將其與布爾值true一同傳入add函數, 這樣就能夠獲得咱們預期的結果:

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函數中不返回tpl, 而是返回咱們動態建立的函數:

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

不要在函數中直接傳入參數, 利用apply方法調用該函數並傳入參數. 這樣纔會建立正確的做用域, this.name纔可正確執行, 此時this指向data對象.

最後咱們還想在其中實現一些複雜的操做, 例如if/else聲明以及循環:

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<a href="#"><%this.skills[index]%></a>' +
'<%}%>';
console.log(TemplateEngine(template, {
    skills: ["js", "html", "css"]
}));複製代碼

不過如今會拋出錯誤Uncaught SyntaxError: Unexpected token for, 經過調試能夠發現問題:

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循環的那行代碼不該該被添加到數組中, 因而咱們這樣進行改進:

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';
}複製代碼

上述代碼添加了一個新的正則表達式, 若是JS代碼以if, for, else, switch, case, break, { , }這些內容爲起始值, 則直接添加該行代碼, 不添加到數組中. 那麼最後的結果就是:

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("");複製代碼

這樣的話, 全部的內容都被正確編譯.

My skills:<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 TemplateEngine = function(html, options) {
    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0, match;
    var add = function(line, js) {
        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
        return add;
    }
    while(match = re.exec(html)) {
        add(html.slice(cursor, match.index))(match[1], true);
        cursor = match.index + match[0].length;
    }
    add(html.substr(cursor, html.length - cursor));
    code += 'return r.join("");';
    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}複製代碼

參考

相關文章
相關標籤/搜索