[TOC]
模板引擎 easyTpl
的实现
概述
项目中经常需要使用js模板去渲染字符串,像 handlebars
这样的模板引擎过于庞大,其实自己完全可以实现一个简单的模板引擎,下面就让我们一起来实现一下吧。
在做之前我们需要知道如何去使用,比如下面的方式:
var data = {
name: 'Chen Danwei',
addr: 'Shenzhen'
};
var tpl = 'Hello, my name is {{name}}. I am in {{addr}}';
var str = easyTpl(tpl, data);
console.log(str);
输出:
'Hello, my name is Chen Danwei. I am in Shenzhen.'
easyTpl
函数需要接收模板字符串和数据两个参数,返回替换变量后的字符串。
如何实现呢?
(1)第一步,尝试写正则表达式,匹配 {{variable}}
和 {{variable.varable}}
形式的字符串,其中 variable
需要满足变量的命名规范。
代码1:
var reg = /{{[a-zA-Z$_][a-zA-Z$_0-9\.]*}}/ig;
var strs = [
'hello{{__}}',
"hello {{}}",
'hello {name}',
'hello {{name.age}}',
'hello {{{good}}',
'hello {{123ok dd}}',
'hello {{ {{dd}}{{ok.dd}}'
];
strs.forEach(function(str) {
console.log(str.match(reg));
});
输出:
{{__}}
null
null
{{name.age}}
{{good}}
null
{{dd}}, {{ok.dd}}
注:上面的测试代码做为我们单元测试的原型,后续单元测试会用到。
(2)第二步,数组遍历,进行字符串替换。
代码2:
function easyTpl(tpl, data) {
var reg = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
return tpl.replace(reg, function(raw, key, index, str) {
return data[key] || raw;
});
}
var strs = [
'hello{{__}}',
"hello {{}}",
'hello {name}',
'hello {{friend.name}}',
'hello {{{age}}',
'hello {{123ok dd}}',
'hello {{sex}} {{sex}} {{sex}} {{name}}'
];
var data = {
name: 'Chen Danwei',
age: 25,
sex: '男',
friend: {
name: 'xiaoming'
}
};
strs.forEach(function(str) {
console.log(easyTpl(str, data));
});
输出:
'hello{{__}}'
'hello {{}}'
'hello {name}'
'hello {{friend.name}}'
'hello {25'
'hello {{123ok dd}}'
'hello 男 男 男 Chen Danwei'
是不是很简单,上面的核心代码 easyTpl
函数仅仅3行代码就能基本满足上面代码1例子中的需求。但是,如果是下面代码3的情况就有问题了。
代码3:
var data = {
name: 'Chen Danwei',
dog: {
color: 'yellow',
age: 3
}
};
var tpl = 'Hello, my name is {{name}}. I have a {{dog.age}} year old {{dog.color}} dog.';
console.log(easyTpl(tpl, data));
// 应输出:'Hello, my name is Chen Danwei. I have a 3 year old yellow dog.'
// 实际输出:'Hello, my name is Chen Danwei. I have a {{dog.age}} year old {{dog.color}} dog.'
此时,代码3例子里的 easyTpl
函数已经无法满足需求了,因为在遍历到 {{dog.age}}
时会执行 data[key]
代替为 data['dog.age']
,而这种写法显然无法得到正确的 age
值。
如何对多层嵌套的JSON对象进行解析呢?
我们可以把模板变量以 .
号进行字符串分割,使用循环访问对应变量的值,如下所示:
代码4:
function easyTpl(tpl, data) {
var reg = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
return tpl.replace(reg, function(raw, key, index, str) {
var paths = key.split('.'),
lookup = data;
while(paths.length > 0) {
lookup = lookup[paths.shift()];
}
return lookup || raw;
});
}
console.log(easyTpl(tpl, data));
输出:
'Hello, my name is Chen Danwei. I have a 3 year old yellow dog.'
完美解决问题,可以把该函数放到项目的通用库里,在简单场景下可以很方便的使用。当然正如这个模板引擎功能还是很弱,如果在复杂的场景下(判断、遍历)使用还需进一步完善。
代码封装
下面的例子演示了如何封装代码,让我们的代码模块化,并且可以在各个端方便调用。
(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 env
context[name] = definition();
}
})('easyTpl', function() {
return function(tpl, data) {
var reg = /{{([a-zA-Z$_][a-zA-Z$_0-9\.]*)}}/g;
return tpl.replace(reg, function(raw, key, index, str) {
var paths = key.split('.'),
lookup = data;
while(paths.length > 0) {
lookup = lookup[paths.shift()];
}
return lookup || raw;
})
}
}, this);
对上面的代码分段讲解:
(function(name, definition, context){})('easyTpl', function() {...}, this);
最外层是一个立即执行函数,用于封装和隔离作用域,传递3个参数分别是:第一个参数是模块名称,第二个参数是模块的具体实现方式,第三个参数是模块当前所处的运行环境(在 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 env
context[name] = definition();
}
如果当前模块运行在node环境下,则遵循 CommonJS
规范,必然存在 module.exports
这个全局变量。上面的代码相当于:
var definition = function() {
return function(tpl, data){...};
}
module.exports = definition();
如果当前模块运行在 AMD
(如 RequireJS
)和 CMD
(如 SeaJS
)规范的框架下,则分别存在 window.define.amd
和 window.define.cmd
这两个变量,而代码 context['define']
中的 context
就是 (function(name, definition, context){})('easyTpl', function() {...}, this);
中的 this
,也就是 window
。所以该部分代码的写法为 CMD
、AMD
规范下模块定义的方式。
define(function() {
return function(tpl, data) {...};
})
如果当前模块运行在普通的浏览器端,则执行 context[name] = definition();
,即 window['easyTpl'] = definition();
。
单元测试
mocha
是一个简单、灵活有趣的 Javascript 测试框架,用于 Node.js
和浏览器上的 Javascript 应用测试。
chai
是一个 BDD/TDD 模式的断言库,可以在 Node.js
和浏览器环境运行,可以搞笑的和任何 Javascript 测试框架搭配使用。
npm install -g mocha
npm install chai
test.js
代码如下:
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');
});
});
});
接着执行 mocha <filename>
命令,就可以自动完成测试了。
mocha test.js