只有 20 行的 JavaScript 模板引擎實例詳解
本文實例講述了 JavaScript 模板引擎。分享給大家供大家參考,具體如下:
原文鏈接:JavaScript template engine in just 20 lines
(譯者吐槽:只收藏不點贊都是耍流氓)
前言我仍舊在為我的JS預處理器AbsurdJS進行開發工作。它原本是一個CSS預處理器,但之后它擴展成為了CSS/HTML預處理器,很快它將支持JS到CSS/HTML的轉換。它就像一個模板引擎一樣能夠生成HTML代碼,也就是說它能夠用數據填充模板當中的標識片段。
因此,我希望去寫一個可以滿足我當前需求的模板引擎。AbsurdJS主要作為NodeJS的模塊使用,但同時它也可以在客戶端使用。為了這個目的,我無法使用市面上已經存在的模板引擎,因為它們幾乎全都依賴于NodeJS,并且難以在瀏覽器中使用。我需要一個更小,純JS寫成的模板引擎。我瀏覽了這篇由John Resig寫的博客,似乎這正是我需要的東西。我把當中的代碼稍作修改,并且濃縮到了20行。
這段代碼的運行原理非常有趣,我將在這篇文章中一步一步為大家展示John的wonderful idea。
1、提取標識片段這是我們在開始的時候將要獲得的東西:
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>
我們要做的第一件事就是獲取模板中的標識片段<%...%>,然后用傳入引擎中的數據去填充它們。我決定用正則表達式去完成這些功能。正則不是我的強項,所以大家將就一下,如果有更好的正則也歡迎向我提出。
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);}
運行,發現所有的標識片段已經被我們獲取到了。
2、數據填充與邏輯處理在獲取了標識片段以后,我們就要對它們進行數據的填充。使用.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='http://www.cgvv.com.cn/bcjs/16666.html'><%this.skills[index]%></a>’ +’<%}%>’;
如果使用字符串拼接,結果將會變成這樣:
return’My skills:’ + for(var index in this.skills) { +’<a href='http://www.cgvv.com.cn/bcjs/16666.html'>’ + this.skills[index] +’</a>’ +}
理所當然這會報錯。這也是我決定參照John的文章去寫邏輯的原因——我把所有的字符串都push到一個數組中,在最后才把它們拼接起來:
var r = [];r.push(’My skills:’); for(var index in this.skills) {r.push(’<a href='http://www.cgvv.com.cn/bcjs/16666.html'>’);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(/[rtn]/g, ’’)).apply(data);
我們甚至不需要向函數傳遞任何的參數,因為apply方法已經為我們完整了這一步工作。它自動設置了作用域,這也是為什么this.name可以運行,this指向了我們的data。
3、代碼優化大致上已經完成了。最后一件事情,我們需要支持更多復雜的表達式,像if/else表達式和循環等。讓我們用同樣的例子去嘗試運行下列代碼:
var template = ’My skills:’ + ’<%for(var index in this.skills) {%>’ + ’<a href='http://www.cgvv.com.cn/bcjs/16666.html#'><%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='http://www.cgvv.com.cn/bcjs/16666.html'>');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='http://www.cgvv.com.cn/bcjs/16666.html#'>');r.push(this.skills[index]);r.push('</a>');}r.push('');return r.join('');
理所當然的正確執行啦:
My skills:<a href='http://www.cgvv.com.cn/bcjs/16666.html#' >js</a><a href='http://www.cgvv.com.cn/bcjs/16666.html#'>html</a><a href='http://www.cgvv.com.cn/bcjs/16666.html#'>css</a>
接下來的修改會給予我們更強大的功能。我們可能會有更加復雜的邏輯會放進模板中,像這樣:
var template = ’My skills:’ + ’<%if(this.showSkills) {%>’ + ’<%for(var index in this.skills) {%>’ + ’<a href='http://www.cgvv.com.cn/bcjs/16666.html#'><%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(/[rtn]/g, ’’)).apply(options);}
優化后的代碼甚至少于15行。
后記(譯者注)這是我第一次完整地翻譯文章,語句多有錯漏還請多多諒解,今后將繼續努力,爭取把更多優質的文章翻譯分享。
由于對前端的框架、模板引擎一類的工具特別感興趣,非常希望能夠學習當中的原理,于是乎找了個相對簡單的模板引擎開刀進行研究,google后看到了這篇文章覺得非常優秀,一步步講解生動且深入,代碼經過本人測試均能正確得到文章描述的結果。
模板引擎有多種設計思路,本文僅僅為其中的一種,其性能等參數還有待測試和提高,僅供學習使用。謝謝大家~
感興趣的朋友可以使用在線HTML/CSS/JavaScript代碼運行工具:http://tools.jb51.net/code/HtmlJsRun測試上述代碼運行效果。
更多關于JavaScript相關內容感興趣的讀者可查看本站專題:《javascript面向對象入門教程》、《JavaScript錯誤與調試技巧總結》、《JavaScript數據結構與算法技巧總結》、《JavaScript遍歷算法與技巧總結》及《JavaScript數學運算用法總結》
希望本文所述對大家JavaScript程序設計有所幫助。
相關文章:
