概述:項(xiàng)目中經(jīng)常需要使用js模版去渲染字符串尖殃,handlebars這樣的模版引擎又過(guò)于龐大,其實(shí)自己可以完全可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的模版引擎,寥寥十幾行代碼而已蹋辅。
本文首先用傳統(tǒng)的方法實(shí)現(xiàn)一個(gè)模版函數(shù);在此基礎(chǔ)上封裝成可在不同環(huán)境(瀏覽器環(huán)境亡蓉、node環(huán)境)晕翠、不同規(guī)范(CMD、AMD)下使用的組件;之后演示了如何把組件上傳到npm庫(kù)(可通過(guò)npm install easyTpl
直接安裝)淋肾;上傳到bower庫(kù)(可通過(guò)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) 第一步,先嘗試寫(xiě)正則表達(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));
});
上面的測(cè)試代碼也是我們單元測(cè)試的原型叹坦,后續(xù)單元測(cè)試會(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}}"
是不是很簡(jiǎn)單募书,上面的核心代碼easyTpl函數(shù)區(qū)區(qū)3行就能能基本滿足上面代碼1例子里的需求绪囱。
但是,如果是下面代碼4的情況就有問(wèn)題了
代碼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)無(wú)法滿足需求鬼吵。因?yàn)樵诒闅v到{{dog.age}}
時(shí)會(huì)執(zhí)行替換,data[key]
即data["dog.age"]
篮赢,而這種寫(xiě)法顯然無(wú)法得到 age
的值齿椅。
如何對(duì)多層嵌套的JSON對(duì)象進(jìn)行解析呢?
我們可以把模版變量以.
號(hào)進(jìn)行字符串分割荷逞,使用循環(huán)訪問(wè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"
完美解決問(wèn)題种远,可以把該函數(shù)放到項(xiàng)目的通用庫(kù)里涩澡,在簡(jiǎn)單場(chǎng)景下可以很方便的使用。當(dāng)然正如這個(gè)模版引擎功能還很弱坠敷,如果在復(fù)雜的場(chǎng)景下(判斷妙同、遍歷)使用還需進(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
。所以該部分代碼的寫(xiě)法為CMD缘缚、AMD
規(guī)范下模塊定義的方式勾笆。
define(function(){
return function(tpl, data){...};
});
如果當(dāng)前模塊運(yùn)行在普通的瀏覽器端,則執(zhí)行context[name] = definition();
桥滨,即window['easyTpl'] = definition();
匠襟。
各環(huán)境demo演示地址:
單元測(cè)試
mocha 是一個(gè)簡(jiǎn)單、靈活有趣的 JavaScript 測(cè)試框架该园,用于 Node.js 和瀏覽器上的 JavaScript 應(yīng)用測(cè)試。
Chai是一個(gè)BDD/TDD模式的斷言庫(kù)帅韧,可以再node和瀏覽器環(huán)境運(yùn)行里初,可以高效的和任何js測(cè)試框架搭配使用。
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