概述:項(xiàng)目中經(jīng)常需要使用js模版去渲染字符串,handlebars這樣的模版引擎又過于龐大,其實(shí)自己可以完全可以實(shí)現(xiàn)一個(gè)簡單的模版引擎,寥寥十幾行代碼而已。
本文首先用傳統(tǒng)的方法實(shí)現(xiàn)一個(gè)模版函數(shù);在此基礎(chǔ)上封裝成可在不同環(huán)境(瀏覽器環(huán)境、node環(huán)境)、不同規(guī)范(CMD、AMD)下使用的組件;之后演示了如何把組件上傳到npm庫(可通過npm install easyTpl直接安裝);上傳到bower庫(可通過bower install easyTpl)下載。
模版引擎easyTpl的實(shí)現(xiàn)
在做之前需要先思考如何去用,比如下面的方式:
代碼1:
var data = {
name: 'ruoyu',
addr: 'Hunger Valley'
};
var tpl = 'Hello, my name is {{name}}. I am in {{addr}}.';
var str = easyTpl(tpl, data);
console.log(str); // 輸出: Hello, my name is ruoyu. I am in Hunger Valley.
上面的例子需要輸出Hello, my name is ruoyu. I am in Hunger Valley.。
因此,easyTpl函數(shù)需要接收模版字符串和數(shù)據(jù)兩個(gè)參數(shù),返回替換變量后的字符串。
如何實(shí)現(xiàn)呢?
(1) 第一步,先嘗試寫正則表達(dá)式,匹配{{variable}}和{{variable.variable}}形式的字符串,其中variable滿足變量的命名格式。
代碼2:
var reg = /{{[a-zA-Z$_][a-zA-Z$_0-9\.]*}}/ig;
var strs = [
''hello{{__}}', //{{__}}
"hello {{}}", //null
'hello {name}', //null
'hello {{name.age}}', //{{name.age}}
'hello {{{good}}', //{{good}}
'hello {{123ok dd}}', //null
'hello {{ {{dd}}{{ok.dd}}' //{{dd}}, {{ok.dd}}
]
strs.forEach(function(str){
console.log(str.match(reg));
});
上面的測試代碼也是我們單元測試的原型,后續(xù)單元測試會(huì)用到。
(2) 第二步, 遍歷字符串,做替換
代碼3:
function easyTpl(tpl, data){
var re = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
return tpl.replace(re, function(raw, key, offset, string){
return data[key]||raw;
});
}
var strs = [
'hello{{__}}',
'hello {name}',
'hello {{friend.name}}',
'hello {{{age}}',
'hello {{123ok dd}}',
'hello {{sex}} {{sex}} {{sex}} {{friend.name}}'
];
var data = {
name: 'hunger',
age: 28,
sex: '男',
friend: {
name: 'xiaoming'
}
};
strs.forEach(function(str){
console.log(easyTpl(str, data));
});
輸出:
"hello{{__}}"
"hello {name}"
"hello {{friend.name}}"
"hello {28"
"hello {{123ok dd}}"
"hello 男 男 男 {{friend.name}}"
是不是很簡單,上面的核心代碼easyTpl函數(shù)區(qū)區(qū)3行就能能基本滿足上面代碼1例子里的需求。
但是,如果是下面代碼4的情況就有問題了
代碼4:
var data = {
name: 'ruoyu',
dog: {
color: 'yellow',
age: 2
}
};
var tpl = 'Hello, my name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog.';
var str = easyTpl(tpl, data);
console.log(str);
// 應(yīng)輸出: Hello, my name is ruoyu. I have a 2 year old yellow dog.
// 實(shí)際輸出: Hello, my name is hunger. I have a {{dog.age}} year old {{dog.color}} dog.
此時(shí),代碼3里的easyTpl函數(shù)已經(jīng)無法滿足需求。因?yàn)樵诒闅v到{{dog.age}}時(shí)會(huì)執(zhí)行替換,data[key]即data["dog.age"],而這種寫法顯然無法得到 age的值。
如何對(duì)多層嵌套的JSON對(duì)象進(jìn)行解析呢?
我們可以把模版變量以.號(hào)進(jìn)行字符串分割,使用循環(huán)訪問對(duì)應(yīng)變量的值。如代碼4所示。
代碼4:
function easyTpl2(tpl, data){
var re = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
return tpl.replace(re, function(raw, key, offset, string){
var paths = key.split('.'),
lookup = data;
while(paths.length>0){
lookup = lookup[paths.shift()];
}
return lookup||raw;
});
}
console.log(easyTpl2(strs[6], data));
//輸出 "Hello, my name is hunger. I have a 2 year old yellow dog"
完美解決問題,可以把該函數(shù)放到項(xiàng)目的通用庫里,在簡單場景下可以很方便的使用。當(dāng)然正如這個(gè)模版引擎功能還很弱,如果在復(fù)雜的場景下(判斷、遍歷)使用還需進(jìn)一步完善。
代碼封裝
下面的例子演示了如何封裝代碼,讓我們的代碼模塊化,并可以在各個(gè)端方便使用。
(function (name, definition, context) {
if (typeof module != 'undefined' && module.exports) {
// in node env
module.exports = definition();
} else if (typeof context['define'] == 'function' && (context['define']['amd'] || context['define']['cmd']) ) {
//in requirejs seajs env
define(definition);
} else {
//in client evn
context[name] = definition();
}
})('easyTpl', function () {
return function (tpl, data){
var re = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
return tpl.replace(re, function(raw, key, offset, string){
var paths = key.split('.'),
lookup = data;
while(paths.length>0){
lookup = lookup[paths.shift()];
}
return lookup||raw;
});
}
}, this);
對(duì)上面的代碼分段講解:
(function (name, definition, context) {})('easyTpl', function () {...}, this);
最外層是一個(gè)立即執(zhí)行函數(shù),用于封裝和隔離作用域,傳遞3個(gè)參數(shù)進(jìn)去。第一個(gè)參數(shù)是模塊名稱,第二個(gè)參數(shù)是模塊的具體實(shí)現(xiàn)方式,第三個(gè)參數(shù)是模塊當(dāng)前所處的作用域(在node端和在瀏覽器端是不同的)。
if (typeof module != 'undefined' && module.exports) {
// in node env
module.exports = definition();
} else if (typeof context['define'] == 'function' && (context['define']['amd'] || context['define']['cmd']) ) {
//in requirejs seajs env
define(definition);
} else {
//in client evn
context[name] = definition();
}
如果當(dāng)前模塊運(yùn)行在node環(huán)境下,則遵循CommonJS規(guī)范,必然存在module.exports這個(gè)全局變量。上面的代碼相當(dāng)于
var definition = function(){
return function(tpl, data){...};
}
module.exports = definition();
如果當(dāng)前模塊運(yùn)行在遵循AMD(如RequireJS)和CMD(如SeaJS) 規(guī)范的框架下,則分別存在window.define.amd和window.define.cmd這兩個(gè)變量,而代碼context['define']中的content就是(function (name, definition, context) {})('easyTpl', function () {...}, this);中的this,也就是window。所以該部分代碼的寫法為CMD、AMD規(guī)范下模塊定義的方式。
define(function(){
return function(tpl, data){...};
});
如果當(dāng)前模塊運(yùn)行在普通的瀏覽器端,則執(zhí)行context[name] = definition();,即window['easyTpl'] = definition();。
各環(huán)境demo演示地址:
單元測試
mocha 是一個(gè)簡單、靈活有趣的 JavaScript 測試框架,用于 Node.js 和瀏覽器上的 JavaScript 應(yīng)用測試。
Chai是一個(gè)BDD/TDD模式的斷言庫,可以再node和瀏覽器環(huán)境運(yùn)行,可以高效的和任何js測試框架搭配使用。
npm install -g mocha
npm install chai
var assert = require('chai').assert,
easyTpl = require('../lib/easyTpl');
var units = [
[
{
name: 'ruoyu',
addr: 'Hunger Valley'
},
'I\'m {{name}}. I live in {{addr}}.',
'I\'m ruoyu. I live in Hunger Valley.'
],
[
{
name: 'ruoyu',
dog: {
color: 'yellow',
age: 2
}
},
'My name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog.',
'My name is ruoyu. I have a 2 year old yellow dog.'
],
[
{
name: 'ruoyu',
dog: {
color: 'yellow',
age: 2,
friend: {
name: 'hui'
}
}
},
'My name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog. His friend is {{dog.friend.name}}.',
'My name is ruoyu. I have a 2 year old yellow dog. His friend is hui.'
]
]
describe('easyTpl', function () {
it('should replace patten correctly', function () {
units.forEach(function(testData, idx){
assert.equal(easyTpl(testData[1], testData[0]), testData[2], 'test ' + idx + ' failed');
});
});
});
提交到NPM Bower
Github 地址: https://github.com/jirengu/easytpl