寫於 2016.06.13javascript
(譯者吐槽:只收藏不點贊都是耍流氓)html
我仍舊在爲個人JS預處理器AbsurdJS進行開發工做。它本來是一個CSS預處理器,但以後它擴展成爲了CSS/HTML預處理器,很快它將支持JS到CSS/HTML的轉換。它就像一個模板引擎同樣可以生成HTML代碼,也就是說它可以用數據填充模板當中的標識片斷。前端
所以,我但願去寫一個能夠知足我當前需求的模板引擎。AbsurdJS主要做爲NodeJS的模塊使用,但同時它也能夠在客戶端使用。爲了這個目的,我沒法使用市面上已經存在的模板引擎,由於它們幾乎全都依賴於NodeJS,而且難以在瀏覽器中使用。我須要一個更小,純JS寫成的模板引擎。我瀏覽了這篇由John Resig寫的博客,彷佛這正是我須要的東西。我把當中的代碼稍做修改,而且濃縮到了20行。java
這段代碼的運行原理很是有趣,我將在這篇文章中一步一步爲你們展現John的wonderful idea。正則表達式
這是咱們在開始的時候將要得到的東西:數組
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 })); 複製代碼
一個簡單的函數,傳入模板和數據做爲參數,正如你所想象的,咱們想要獲得如下的結果:瀏覽器
<p>Hello, my name is Krasimir. I'm 29 years old.</p> 複製代碼
咱們要作的第一件事就是獲取模板中的標識片斷<%...%>
,而後用傳入引擎中的數據去填充它們。我決定用正則表達式去完成這些功能。正則不是個人強項,因此你們將就一下,若是有更好的正則也歡迎向我提出。bash
var re = /<%([^%>]+)?%>/g;
複製代碼
咱們將會匹配全部以<%
開頭以%>
結尾的代碼塊,末尾的g(global)表示咱們將匹配多個。有許多的方法可以用於匹配正則,可是咱們只須要一個可以裝載字符串的數組就夠了,這正是exec所作的工做:數據結構
var re = /<%([^%>]+)?%>/g;
var match = re.exec(tpl);
複製代碼
在控制檯console.log(match)
能夠看到:
[
"<%name%>",
" name ",
index: 21,
input:
"<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
]
複製代碼
咱們取得了正確的匹配結果,但正如你所看到的,只匹配到了一個標識片斷<%name%>
,因此咱們須要一個while
循環去取得全部的標識片斷。
var re = /<%([^%>]+)?%>/g, match;
while(match = re.exec(tpl)) {
console.log(match);
}
複製代碼
運行,發現全部的標識片斷已經被咱們獲取到了。
在獲取了標識片斷之後,咱們就要對它們進行數據的填充。使用.replace
方法就是最簡單的方式:
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 = {
name: "Krasimir Tsonev",
age: 29
}
複製代碼
OK,正常運行。但很明顯這並不足夠,咱們當前的數據結構很是簡單,但實際開發中咱們將面臨更復雜的數據結構:
{
name: "Krasimir Tsonev",
profile: { age: 29 }
}
複製代碼
出現錯誤的緣由,是當咱們在模板中輸入<%profile.age%>
的時候,咱們獲得的data["profile.age"]
是undefined的。顯然.replace
方法是行不通的,咱們須要一些別的方法把真正的JS代碼插入到<%和%>當中,就像如下栗子:
var template = '<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
是個真正的函數,它包含一個參數,其函數體爲console.log(arg + 1)
。以上代碼等價於下列代碼:
var fn = function(arg) {
console.log(arg + 1);
}
fn(2); // 輸出 3
複製代碼
經過new Function
,咱們得以經過字符串去建立一個函數,這正是咱們所須要的。在建立這麼一個函數以前,咱們須要去構造這個它的函數體。該函數體應當返回一個最終拼接好了的模板。沿用前文的模板字符串,想象一下這個函數應當返回的結果:
return
"<p>Hello, my name is " +
this.name +
". I\'m " +
this.profile.age +
" years old.</p>";
複製代碼
顯然,咱們把模板分紅了文本和JS代碼。正如上述代碼,咱們使用了簡單的字符串拼接的方式去獲取最終結果,可是這個方法沒法100%實現咱們的需求,由於以後咱們還要處理諸如循環之類的JS邏輯,像這樣:
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>' +
}
複製代碼
理所固然這會報錯。這也是我決定參照John的文章去寫邏輯的緣由——我把全部的字符串都push到一個數組中,在最後才把它們拼接起來:
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
表示咱們正處於模板的哪一個位置,咱們須要它去遍歷全部的字符串,跳過填充數據的片斷。另外,add
函數的任務是把字符串插入到code
變量中,做爲構建函數體的過程方法。這裏有一個棘手的地方,咱們須要跳過標識符<%%>
,不然當中的JS腳本將會失效。若是咱們直接運行上述代碼,結果將會是下面的狀況:
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; } 複製代碼
標識片斷中的內容將經過一個boolean值進行控制。如今咱們獲得了一個正確的函數體:
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("");
複製代碼
接下來咱們要作的就是生成這個函數而且運行它。在這個模板引擎的末尾,咱們用如下代碼去代替直接返回一個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
。仔細觀察,經過code
變量咱們能夠找出問題所在:
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
循環的代碼不該該被push到數組當中,而是直接放在腳本里面。爲了解決這個問題,在把代碼push到code
變量以前咱們須要多一步的判斷:
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, |
開頭,那它們將會直接添加到函數體中;若是不是,則會被push到code
變量中。下面是修改後的結果:
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); } 複製代碼
優化後的代碼甚至少於15行。
這是我第一次完整地翻譯文章,語句多有錯漏還請多多諒解,從此將繼續努力,爭取把更多優質的文章翻譯分享。
因爲對前端的框架、模板引擎一類的工具特別感興趣,很是但願可以學習當中的原理,因而乎找了個相對簡單的模板引擎開刀進行研究,google後看到了這篇文章以爲很是優秀,一步步講解生動且深刻,代碼通過本人測試均能正確獲得文章描述的結果。
模板引擎有多種設計思路,本文僅僅爲其中的一種,其性能等參數還有待測試和提升,僅供學習使用。 謝謝你們~