20行實現javascript模板引擎
我仍然在用AbsurdJS預處理器寫javascript。起初,這只是一個CSS預處理器,後來我把它擴展爲CSS/HTML預處理器。最近,它可以實現javascript到CSS/HTML的轉換,因爲它可以作爲模板引擎來生成HTML。比如,可以用數據填充HTML模板。
然後,我就想寫一個簡單的模板引擎可以完美地與我當前的開發工作相配合。AbsurdJS主要是作爲nodejs模塊來發布的,但是它也可以作爲客戶端使用。有了這種想法,我意識到我不能利用現有的模板引擎。因爲現在的大多數模板引擎只是基於nodejs,很難把他們複製到瀏覽器來使用。我需要一個體積小的用原生JS寫的模板引擎。我曾經拜讀過John Resig的一篇文章JavaScript Micro-Templating。這好像就是我所需要的。我把裏面的代碼做了些許變動,使之縮減爲20行。我想這腳本的運行機制是非常有趣的。本文中,我一步步地重新創建一個模板引擎,然後你就會體會到來自John的偉大創意。
我們以下面的代碼作爲開始吧:
- 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>"
- ]
但是我們只得到了一個,再來改善一下:
- var TemplateEngine = function(tpl, data) {
- var re = /<%([^%>]+)?%>/g;
- while(match = re.exec(tpl)) {
- tpl = tpl.replace(match[0], data[match[1]])
- }
- return tpl;
- }
OK,我們的最初目標達到了,但這遠遠不夠。這隻能容易獲取到data['property']。但在實踐中,我們可能遇到複雜的嵌套對象。比如:
- {
- name: "Krasimir Tsonev",
- profile: { age: 29 }
- }
我們先前所做的工作就這樣失效了!
那我們就分析一下其他的情況。比如,我們有個這樣的模板:
- var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
再或者我們可能還見過這樣的模板:
- var template =
- 'My skills:' +
- '<%for(var index in this.skills) {%>' +
- '<a href=""><%this.skills[index]%></a>' +
- '<%}%>';
這該怎麼辦呢? John採用了 new Function 語法實現——可以從字符串來創建函數。
那我們先來熟悉一下這種語法。看一個簡單的例子:
- var fn = new Function("arg", "console.log(arg + 1);");
- fn(2); // outputs 3
上述代碼創建的函數fn等價於:
- function fn(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>";
而對於
- 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>' +
- }
當然我們所設想的會產生語法錯誤,那我們可以改變一下:
- 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('');
有了預想結果,下一步就是分別處理每一行來產生自定義函數。
在進行處理每一行的時候,我們應該考慮到以下問題:
- 引號的轉義,否則產生的腳本不可用
- <% %>裏面的字符不應該被當做字符串處理
- var TemplateEngine = function(tpl, data) {
- var re = /<%([^%>]+)?%>/g,
- code = 'var r=[];\n',
- cursor = 0;
- 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 }
- }));
考慮到if/else等JS語句,我們再做優化:
- var TemplateEngine = function(html, options) {
- 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 += 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);
- }
終極目標實現!!
詳見最終版本
原文作者:http://krasimirtsonev.com/blog/article/Javascript-template-engine-in-just-20-line