本文同步自我的博客园:http://www.cnblogs.com/hustskyking/
一、媒介
什么是模板引擎,说的简朴点,就是一个字符串中有几个变量待定。比方:
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
经由历程模板引擎函数把数据塞进去,
var data = {
"name": "Barret Lee",
"age": "20"
};
var result = tplEngine(tpl, data);
//Hei, my name is Barret Lee, and I'm 20 years old.
那这玩艺儿有什么作用呢?实在他就是一个预处置惩罚器(preprocessor),搞php开辟的童鞋对Smarty必定是异常熟习,Smarty是一个php模板引擎,tpl中待处置惩罚的字符经由历程数据婚配然后输出响应的html代码,加上比较给力的缓存手艺,其速率和易用性是异常给力的!JS Template也是一样的,我们的数据库里保留着数以万万计的数据,而每一条数据都是经由历程同一种体式格局输入,就拿上面的例子来讲,我们不可能在数据库里存几千条”Hei, my name…”,而是只保留对应的name和age,经由历程模板输出效果。
JS模板引擎应当做哪些事变?看看下面一串代码:
var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' +
'var post = posts[i]; %>' +
'<% if(!post.expert){ %>' +
'<span>post is null</span>' +
'<% } else { %>' +
'<a href="#"><% post.expert %> at <% post.time %></a>' +
'<% } %>' +
'<% } %>';
一个基础的模板引擎最少能够保证上面的代码能够平常剖析。如送入的数据是:
var data = {
"posts": [{
"expert": "content 1",
"time": "yesterday"
},{
"expert": "content 2",
"time": "today"
},{
"expert": "content 3",
"time": "tomorrow"
},{
"expert": "",
"time": "eee"
}]
};
能够输出:
<a href="#">content 1 at yesterday</a>
<a href="#">content 2 at today</a>
<a href="#">content 3 at tomorrow</a>
<span>post is null</span>
下面就详细说说这个模板引擎的道理是啥样的。
二、JS模板引擎的完成道理
1.正则抠出要婚配的内容
针对这一串代码,经由历程正则猎取内容
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var data = {
"name": "Barret Lee",
"age": "20"
};
最简朴的体式格局就是经由历程replace函数了:
var result = tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
return data[s1];
});
经由历程正则替代,我们很轻松的拿到了result,你能够去试一试,他正式我们想要的效果。然则这里又有了一个题目,改一下data和tpl,
var tpl = 'Hei, my name is <%name%>, and I\'m <%info.age%> years old.';
var data = {
"name": "Barret Lee",
"info": { age": "20"}
};
再用上面的体式格局去猎取效果,呵呵,不可了吧~ 这里data[“info.age”]自身就是undefined,所以我们须要换一种体式格局来处置惩罚这个题目,那就是将它转换成真正的JS代码。如:
return 'Hei, my name is ' + data.name + ', and I\'m ' + data.info.age' + ' years old.'
然则接着又有一个题目来了,当我们的代码中涌现for轮回和if的时刻,上面的转换显著是不起作用的,如:
var tpl = 'Posts: ' +
'<% for(var i = 0; i < post.length; i++) {'+
'<a href="#"><% post[i].expert %></a>' +
'<% } %>'
假如继承采纳上面的体式格局,获得的效果就是:
return 'Posts: ' +
for(var i = 0; i < post.length; i++) { +
'<a href="#">' + post[i].exper + '</a>' +
}
这明显不是我们缘由看到的,轻微视察一下上面的组织,假如能够返回一个如许的效果也挺不错哦:
'Posts: '
for(var i = 0; i < post.length; i++) {
'<a href="#">' + post[i].exper + '</a>'
}
然则我们须要获得的是一个字符串,而不是上面如许零星的片断,因而能够把这些东西装入数组中。
2.装入数组
var r = [];
r.push('Posts: ' );
r.push(for(var i = 0; i < post.length; i++) {);
r.push('<a href="#">');
r.push(post[i].exper);
r.push('</a>');
r.push(});
有人看到上面的代码就要笑了,第三行和末了一行代码的逻辑显著是不正确的嘛,那肿么办呢?呵呵,很简朴,不放进去就好了呗,
var r = [];
r.push('Posts: ' );
for(var i = 0; i < post.length; i++) {
r.push('<a href="#">');
r.push(post[i].exper);
r.push('</a>');
}
如许的逻辑就异常完美了,不存在太多的破绽,然则这个转化的历程是怎样完成的?我们必需照样要写一个剖析的模板函数出来。
3.区分js逻辑部份
var r = [];
tpl.replace(/<%([^%>]+)?%>/g, function(s0, s1){
//完蛋了,这里貌似又要回到上面那好笑的逻辑有毛病的一步啦... 该怎样处置惩罚比较好?
});
完蛋了,这里貌似又要回到上面那好笑的逻辑有毛病的一步啦… 该怎样处置惩罚比较好?我们晓得,JS给我们供应了组织函数的“类”,
var fn = new Function("data",
"var r = []; for(var i in data){ r.push(data[i]); } return r.join(' ')");
fn({"name": "barretlee", "age": "20"}); // barretlee 20
晓得了这个就好办了,我们能够把逻辑部份和非逻辑部份的代码链接成一个字符串,然后运用相似fn的函数直接编译代码。而/<%([^%>]+)?%>/g
,这一个正则只能把逻辑部份婚配出来,要想把一切的代码都组合到一同,必需还得婚配非逻辑部份代码。replace函数虽然很壮大,他也能够完成这个使命,然则完成的逻辑比较艰涩,所以我们换别的一种体式格局来处置惩罚。
先看一个简朴的例子:
var reg = /<%([^%>]+)?%>/g;
var tpl = 'Hei, my name is <%name%>, and I\'m <%age%> years old.';
var match = reg.exec(tpl);
console.log(match);
看到的是:
[
0: "<%name%>",
1: name,
index: 16,
input: "Hei, my name is <%name%>, and I'm <%age%> years old."
length: 2
]
这。。。我们但是想获得一切的婚配啊,他居然只猎取了name而疏忽了背面的age,好吧,对正则轻微熟习点的童鞋一定会晓得应当如许处置惩罚:
var reg = /<%([^%>]+)?%>/g;
while(match = reg.exec(tpl)) {
console.log(match);
}
关于正则表达式的内容就不在这里细说了,有兴致的同砚能够多去了解下match,exec,search等正则的相干函数。这里主如果靠match的index属性来定位遍历位置,然后运用while轮回猎取一切的内容。
4.引擎函数
所以我们的引擎函数雏形差不多就出来了:
var tplEngine = function(tpl, data){
var reg = /<%([^%>]+)?%>/g,
code = 'var r=[];\n',
cursor = 0; //重要的作用是定位代码末了一截
var add = function(line) {
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};
while(match = reg.exec(tpl)) {
add(tpl.slice(cursor, match.index)); //增加非逻辑部份
add(match[1]); //增加逻辑部份 match[0] = "<%" + match[1] + "%>";
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor)); //代码的末了一截 如:" years old."
code += 'return r.join("");'; // 返回效果,在这里我们就拿到了装入数组后的代码
console.log(code);
return tpl;
};
如许一来,测试一个小demo:
var tpl = '<% for(var i = 0; i < this.posts.length; i++) {' +
'var post = posts[i]; %>' +
'<% if(!post.expert){ %>' +
'<span>post is null</span>' +
'<% } else { %>' +
'<a href="#"><% post.expert %> at <% post.time %></a>' +
'<% } %>' +
'<% } %>';
tplEngine(tpl, data);
返回的效果让人很惬意:
var r=[];
r.push("");
r.push(" for(var i = 0; i < this.posts.length; i++) {var post = posts[i]; ");
r.push("");
r.push(" if(!post.expert){ ");
r.push("<span>post is null</span>");
r.push(" } else { ");
r.push("<a href=\"#\">");
r.push(" post.expert ");
r.push(" at ");
r.push(" post.time ");
r.push("</a>");
r.push(" } ");
r.push("");
r.push(" } ");
r.push("");
return r.join("");
不过我们并须要for,if,switch等这些东西也push到r数组中去,所以呢,还得改良下上面的代码,假如在line中发现了包括js逻辑的代码,我们就不应当让他进门:
regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
var add = function(line, js) {
js? code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n' :
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
};
所以我们只剩下末了一步工作了,把data扔进去!
5.把data扔进去
没有比完成这东西更简朴的事变啦,经由历程上面临Function这个函数的解说,人人应当也晓得怎样做了。
return new Function(code).apply(data);
运用apply的作用就是让code中的一些变量作用域绑定到data上,不然作用域就会跑到global上,如许获得的数据索引就会出题目啦~ 固然我们能够再优化一下:
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
把回车换行以及tab键都给婚配掉,让代码越发清洁一点。那末终究的代码就是:
var tplEngine = function(tpl, data) {
var reg = /<%([^%>]+)?%>/g,
regOut = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var r=[];\n',
cursor = 0;
var add = function(line, js) {
js? (code += line.match(regOut) ? line + '\n' : 'r.push(' + line + ');\n') :
(code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
while(match = reg.exec(tpl)) {
add(tpl.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
};
三、运用场景
毕竟是前端代码,所以写出来是要为前端效劳的,日常平凡我们处置惩罚的平常是一个html的模板,一般的状况下,模板代码是放在script标签或许textarea中,所以首先是要猎取到这里头的东西,然后再来做剖析。
var barretTpl = function(str, data){
//猎取元素
var element = document.getElementById(str);
if (element) {
//textarea或input则取value,别的状况取innerHTML
var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;
return tplEngine(html, data);
} else {
//是模板字符串,则天生一个函数
//假如直接传入字符串作为模板,则可能变化过量,因而不斟酌缓存
return tplEngine(str, data);
}
var tplEngine = function(tpl, data) {
// content above
};
};
如许一来就越发简朴了,运用体式格局就是 barretTpl(str, data)
, 这里的str能够是模板代码,也能够是一个DOM元素的id~
能够看看这两段代码:https://gist.github.com/barretlee/7765698, https://gist.github.com/barretlee/7765587
也能够戳这个demo
四、优化以及功用拓展
统共就三四十行代码,完成的东西肯定是一个简约版的,不过关于一个简朴的页面而言,这几行代码已充足运用了,假如还想对他做优化,能够从这几个方面斟酌:
- 优化猎取的模板代码,比方去掉行尾空格等
- 标记转义,假如我们想输出
<span>hehe</span>
相似如许的源代码,在push之前必需举行转义 - 代码缓存,假如一个模板会常常运用,能够将它用一个数组缓存在barretTpl闭包内
- 用户本身设置分隔符
五、参考资料
- http://tech.pro/tutorial/1743/javascript-template-engine-in-just-20-lines Krasimir Tsonev
- http://tangram.baidu.com/BaiduTemplate/ JS template