編寫一個簡單的JavaScript模板引擎
隨著Nodejs的流行,JavaScript在前端和后端都開始流行起來。有許多成熟的JavaScript模板引擎,例如 Swig,既可以用在后端,又可以用在前端。
不過很多時候,前端模板僅僅需要簡單地創建一個HTML片段,用Swig這種全功能模板有點大材小用。我們來嘗試自己編寫一個簡單的前端模板引擎,實際上并不復雜。
在編寫前端模板引擎代碼之前,我們應該想好如何來調用它,即這個模板引擎的接口應該是什么樣的。我們希望這樣調用它:
// 創建一個模板引擎: var tpl = new Template('<p>Today: { date }</p>\n<a href="/{ user.id|safe }">{ user.company }</a>'); // 渲染得到HTML片段: var model = { date: 20150316, user: { id: 'A-000&001', company: 'AT&T' } }; var html = tpl.render(model); console.log(html); // <p>Today: 20150316</p> // <a href="/A-000&001">AT&T</a>
因此,一個模板引擎就是把一個字符串中的變量用model的變量替換掉,就完成了。
像Swig這種類Jinja2的模板引擎,它可以替換{{ model.prop }}這樣的變量。
我們選用{ model.prop }來實現我們自己的變量替換,基本思想是用一個正則表達式來匹配{ xxx.xxx }:
var re = /\{\s*([a-zA-Z\.\_0-9()]+)\s*\}/m var match = re.exec('a { template } string');
如果正則匹配成功,則match不為空,match[0]是匹配到的字符串{ template },match[1]是捕獲的變量template,match.index是匹配的索引。
只要不斷地匹配到變量,然后用model的內容替換,就可以得到最終的HTML。但是,分析user.addr.zipcode然后去model中查找并不容易。而且,模板應該可以預編譯,這樣,后續渲染速度就會很快。
JavaScript允許用new Function('source')來通過字符串創建一個函數,這個函數和我們用function ()定義的函數是一模一樣的,因此,一個模板引擎的編譯過程就是創建一個函數,然后調用該函數就實現了模板渲染。
需要編譯的函數代碼應該像這樣:
function () { var r = []; r.push('<p>Today: '); r.push(this.date); r.push('</p>\n<a href="/'); r.push(this.user.id); r.push('">'); r.push(this.user.company); r.push('</a>'); return r.join(''); }
注意到變量名從variable.prop變成了this.variable.prop,是因為調用該函數時我們會把model綁定到this變量上。
因此,模板引擎的代碼如下:
function Template(tpl) { var fn, match, code = ['var r=[];'], re = /\{\s*([a-zA-Z\.\_0-9()]+)\s*\}/m, addLine = function (text) { code.push('r.push(\'' + text.replace(/\'/g, '\\\'').replace(/\n/g, '\\n').replace(/\r/g, '\\r') + '\');'); }; while (match = re.exec(tpl)) { if (match.index > 0) { addLine(tpl.slice(0, match.index)); } code.push('r.push(this.' + match[1] + ');'); tpl = tpl.substring(match.index + match[0].length); } addLine(tpl); code.push('return r.join(\'\');'); // 創建函數: fn = new Function(code.join('\n')); // 用render()調用函數并綁定this參數: this.render = function (model) { return fn.apply(model); }; }
現在,這個簡單的模板引擎已經可以工作了。但是它還有幾個小問題需要解決,一是默認的變量在替換時應該做HTML轉義,二是如果某些不需要轉義的變量,可以用{ user.id|safe }這樣的表達式表示user.id無需轉義。
經過HTML轉義和{ variable|safe }處理的最終代碼如下:
function Template(tpl) { var fn, match, code = ['var r=[];\nvar _html = function (str) { return str.replace(/&/g, \'&\').replace(/"/g, \'"\').replace(/\'/g, \''\').replace(/</g, \'<\').replace(/>/g, \'>\'); };'], re = /\{\s*([a-zA-Z\.\_0-9()]+)(\s*\|\s*safe)?\s*\}/m, addLine = function (text) { code.push('r.push(\'' + text.replace(/\'/g, '\\\'').replace(/\n/g, '\\n').replace(/\r/g, '\\r') + '\');'); }; while (match = re.exec(tpl)) { if (match.index > 0) { addLine(tpl.slice(0, match.index)); } if (match[2]) { code.push('r.push(String(this.' + match[1] + '));'); } else { code.push('r.push(_html(String(this.' + match[1] + ')));'); } tpl = tpl.substring(match.index + match[0].length); } addLine(tpl); code.push('return r.join(\'\');'); fn = new Function(code.join('\n')); this.render = function (model) { return fn.apply(model); }; }
現在就可以用我們預設的代碼來使用這個模板引擎了。不過,把模板寫在字符串中也不是一個好辦法。最佳解決方案是利用<script>標簽,把模板寫在里面,注意一定要加上type="text/plain":
<script id="tpl" type="text/plain"> <p>Today: { date }</p> <a href="/{ user.id|safe }">{ user.company }</a> </script>
然后,用jQuery來獲得模板內容并渲染:
var tpl = new Template($('#tpl').html()); var s = tpl.render({ date: 20150101, user: { id: 'A-000&001', company: 'AT&T' } }); $('#other').html(s);
這樣,我們就用不到30行代碼實現了一個簡單的JavaScript模板引擎。
來自:http://www.liaoxuefeng.com/article/001426512790239f83bfb47b1134b63b09a57548d06e5c5000