本文首發於我的博客: 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.name
和this.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);
}複製代碼
參考