概述:项目中经常需要使用js模版去渲染字符串,handlebars这样的模版引擎又过于庞大,其实自己可以完全可以实现一个简单的模版引擎,寥寥十几行代码而已。
本文首先用传统的方法实现一个模版函数;在此基础上封装成可在不同环境(浏览器环境、node环境)、不同规范(CMD、AMD)下使用的组件;之后演示了如何把组件上传到npm库(可通过npm install easyTpl
直接安装);上传到bower库(可通过bower install easyTpl
)下载。
模版引擎easyTpl
的实现
在做之前需要先思考如何去用,比如下面的方式:
代码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函数需要接收模版字符串和数据两个参数,返回替换变量后的字符串。
如何实现呢?
(1) 第一步,先尝试写正则表达式,匹配{{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));
});
上面的测试代码也是我们单元测试的原型,后续单元测试会用到。
(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函数区区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);
// 应输出: Hello, my name is ruoyu. I have a 2 year old yellow dog.
// 实际输出: Hello, my name is hunger. I have a {{dog.age}} year old {{dog.color}} dog.
此时,代码3里的easyTpl函数
已经无法满足需求。因为在遍历到{{dog.age}}
时会执行替换,data[key]
即data["dog.age"]
,而这种写法显然无法得到 age
的值。
如何对多层嵌套的JSON对象进行解析呢?
我们可以把模版变量以.
号进行字符串分割,使用循环访问对应变量的值。如代码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"
完美解决问题,可以把该函数放到项目的通用库里,在简单场景下可以很方便的使用。当然正如这个模版引擎功能还很弱,如果在复杂的场景下(判断、遍历)使用还需进一步完善。
代码封装
下面的例子演示了如何封装代码,让我们的代码模块化,并可以在各个端方便使用。
(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);
对上面的代码分段讲解:
(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 evn
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']
中的content
就是(function (name, definition, context) {})('easyTpl', function () {...}, this);
中的this
,也就是window
。所以该部分代码的写法为CMD、AMD
规范下模块定义的方式。
define(function(){
return function(tpl, data){...};
});
如果当前模块运行在普通的浏览器端,则执行context[name] = definition();
,即window['easyTpl'] = definition();
。
各环境demo演示地址:
单元测试
mocha 是一个简单、灵活有趣的 JavaScript 测试框架,用于 Node.js 和浏览器上的 JavaScript 应用测试。
Chai是一个BDD/TDD模式的断言库,可以再node和浏览器环境运行,可以高效的和任何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