1.什么是模板引擎
概念:模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档。
原理:就我个人的理解模板的诞生是为了将显示与数据分离,其本质是将模板文件和数据通过模板引擎生成最终的HTML代码(如图)
模板引擎的基本机理就是替换(转换),将指定的标签转换为需要的业务数据;将指定的伪语句按照某种流程来变换输出。比如要实现如下的页面要求:
1.数据来自一个数组 songs;
2.不能写死在页面里
遍历拼接HTML字符串
遍历构造DOM对像
- 如何通过模板引擎来做到呢?先看下一个简单的置换型模板引擎代码
var template = '<p>Hello,my name is <%name%>. I am <%age%> years old.</p>';
var data ={
name:'zyn',
age:31
}
var TemplateEngine = function (tpl,data){
while(match = regex.exec(tpl)){
tpl = tpl.replace(match[0],data[match[1]])
}
return tpl
}
var string = TemplateEngine(template,data)
console.log(string);
我们来一行一行的解释下代码:
var template = '<p>Hello,my name is <%name%>. I am <%age%> years old.</p>';
var data ={
name:'zyn',
age:31
}
写一个P标签包裹的字符串,我想展示的是把data里的name,age 里的值替换到字符串里;
var regex = /<%([^%>]+)?%>/g;
全局匹配以<%开头,中间不是%或>并以%>结尾的配配项,(如图)
如图所示:如果我们选中了匹配后的数组的第2项即下标为1的 name 和age 并利用data把值传进去就可以达成我们的目标了
var TemplateEngine = function (tpl,data){
while(match = regex.exec(tpl)){//(1)注意这是=号付值
tpl = tpl.replace(match[0],data[match[1]])//(2)
}
return tpl//(3)
}
(1)第一次循环 match =["<%name%>", "name"]
(2)tpl =tpl.replace('<%name%>','zyn')
(3)返回tpl='<p>Hello,my name is zyn. I am <%age%> years old.</p>'
(1)第二次循环 match =["<%age%>", "age"]
(2)tpl =tpl.replace('<%age%>','31')
(3)返回tpl='<p>Hello,my name is zyn. I am 31 years old.</p>'
再次匹配没有匹配项null;
再把匹配的字符串放入页面就是我们想要的结果的 document.body.innerHTML= string
- 小结
模板文件: var template = '<p>Hello,my name is <%name%>. I am <%age%> years old.</p>';
数据: var data ={
name:'zyn',
age:31
}
模板引擎: var TemplateEngine = function (tpl,data){
var regex = /<%([^%>]+)?%>/g;
while(match = regex.exec(tpl)){
tpl = tpl.replace(match[0],data[match[1]])
}
return tpl
}
HTML文件:
var string=TemplateEngine(template,data)
document.body.innerHTML= string
2. 复杂逻辑模板引擎
- 问题:
(1).上面以data[“property”]的方式使用了一个简单对象来传递数据,但是实际情况下我们很可能需要更复杂的嵌套对象。所以我们稍微修改了一下data对象:
var data ={
name:'zyn',
profile:{age:31}
}
(2).这样子写的话就实现不了我们想要的替换了,因为在模板中使用<%profile.age%>的话,代码会被替换成data[‘profile.age’],结果是undefined。这样我们就不能简单地用replace函数,而是要用别的方法。如果能够在<%和%>之间直接使用Javascript代码就最好了,这样就能对传入的数据直接求值,像下面这样:
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>'
- 预备知识:
你可能会好奇,这是怎么实现的?这里使用了new Function的语法,根据字符串创建一个函数。我们不妨来看个例子:
var fn = new Function("num", "console.log(num + 1);");
fn(2); //3
fn可是一个货真价实的函数。它接受一个参数,函数体是console.log(arg + 1);。上述代码等价于下面的代码:
var fn = function(num) {
console.log(num + 1);
}
fn(2); // 3
(3).通过这种方法,我们可以根据字符串构造函数,包括它的参数和函数体。在构造函数之前,我们先来看看函数体是什么样子的。按照之前的想法,这个模板引擎最终返回的应该是一个编译好的模板。还是用之前的模板字符串作为例子,那么返回的内容应该类似于:
return
"<p>Hello, my name is " +
this.name +
". I\'m " +
this.profile.age +
" years old.</p>";
(4).当然,这个代码不能直接跑,跑了会出错。所以把所有的字符串放在一个数组里,在程序的最后把它们拼接起来。
var Arr=[];
Arr.push("<p>Helloe,my name is");
Arr.push(this.name);
Arr.push("i am");
Arr.push(this.proflie.age)
Arr.push("years old</p>")
return Arr.join('')
下一步就是收集模板里面不同的代码行,用于生成函数。通过前面介绍的方法,我们可以知道模板中有哪些占位符或者说正则表达式的匹配项,以及它们的位置。所以,依靠一个辅助变量(cursor,游标),我们就能得到想要的结果。来看下代码
var TemplateEngine = function(tpl, data) {
var re = /<%([^%>]+)?%>/g,
code = 'var Arr=[];\n',
cursor = 0;
var add = function(line) {
code += 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n';
}
while(match = re.exec(tpl)) {
add(tpl.slice(cursor, match.index));
add(match[1]);
cursor = match.index + match[0].length;
}
add(tpl.substr(cursor, tpl.length - cursor));
code += 'return Arr.join("");'; // <-- return the result
console.log(code);
return tpl;
}
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
var data = {
name: "zyn",
profile: { age: 29 }
}
console.log(TemplateEngine(template, data));
我们来一行行的解释下代码:
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
var data = {
name: "zyn",
profile: { age: 29 }
}
生成模板文件跟数据;
var re = /<%([^%>]+)?%>/g,
code = 'var Arr=[];\n',
cursor = 0;
正则全局匹配以<%开头,中间不是%或>并以%>结尾的配配项
code保存函数体
游标cursor告诉我们当前解析到了模板中的哪个位置。我们需要依靠它来遍历整个模板字符串
var add = function(line) {
code += 'Arr.push("' +line.replace(/"/g, '\\"') + '");\n';///code包含的双引号字符进行转义
}
函数add,它负责把解析出来的代码行添加到变量code中去。有一个地方需要特别注意,那就是需要把code包含的双引号字符进行转义(escape)。否则生成的函数代码会出错。
while(match = re.exec(tpl)) {
add(tpl.slice(cursor, match.index));
add(match[1]);
cursor = match.index + match[0].length;
}
第一次循环:match=[
0:<%this.name%>",
1:"this.name",
index:21,
input:"<p>Hello, my name is<%this.name%>.I'm<%this.profile.age%>years old.</p>",
length:2
]
tpl.slice(cursor, match.index) = "<p>Hello, my name is "
执行函数add("<p>Hello, my name is ")
code=
"
var Arr=[];
Arr.push("<p>Hello, my name is ");
"
在执行add(match[1]);match[1]="this.name"
code =
"
var Arr=[];
Arr.push("<p>Hello, my name is ");
Arr.push("this.name");
"
cursor = match.index + match[0].length;
cursor = 21+13=34;//就是<%this.name%>最后一位的位置;
第二次循环跟第一次一样继续把模板文件添加到code上;两次循环完成后code =
"
var Arr[];
Arr.push("<p>Hello, my name is ");
Arr.push("this.name");
Arr.push(". I'm ");
Arr.push("this.profile.age")
"
cursor =60 ;
然后执行: add(tpl.substr(cursor, tpl.length - cursor));
cursor =60 ; tpl.length=75
tpl.substr(cursor, tpl.length - cursor)
截取最后一段模板文件 years old.</p>
code += 'return Arr.join("");'
code =
"
var Arr[];
Arr.push("<p>Hello, my name is ");
Arr.push("this.name");
Arr.push(". I'm ");
Arr.push("this.profile.age")
Arr.push("years old </p>")
return Arr.join("")
"
如果还不明白可以复制代码在代码上打几个断点看下执行的过程,很快就能明白;
最后我们会在控制台里面看见如下的内容:
var Arr[];
Arr.push("<p>Hello, my name is ");
Arr.push("this.name");
Arr.push(". I'm ");
Arr.push("this.profile.age")
Arr.push("years old </p>")
return Arr.join("")
<p>Hello, my name is <%this.name%>. I'm <%this.profile.age%> years old.</p>
this.name和this.profile.age不应该有引号啊,所以我们再来改改。
var add = function(line, js) {
js? code += 'Arr.push(' + line + ');\n' ://改动1
code += 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n';
}
while(match = re.exec(tpl)) {
add(tpl.slice(cursor, match.index));
add(match[1],true);//改动2
cursor = match.index + match[0].length;
}
改动1:三木运算,如果是js 就执行 code += 'Arr.push(' + line + ');\n' 否则执行 code += 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n';
改动2:add(match[1],true);告诉函数add这次传入的是js.
占位符的内容和一个布尔值一起作为参数传给add函数,用作区分。这样就能生成我们想要的函数体了。
var Arr=[];
Arr.push("<p>Hello, my name is ");
Arr.push(this.name);
Arr.push(". I'm ");
Arr.push(this.profile.age);
return Arr.join("");
剩下来要做的就是创建函数并且执行它。因此,在模板引擎的最后,把原本返回模板字符串的语句替换成如下的内容:
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
我们甚至不需要显式地传参数给这个函数。我们使用apply方法来调用它。它会自动设定函数执行的上下文。这就是为什么我们能在函数里面使用this.name。这里this指向data对象。
模板引擎接近完成了,不过还有一点,我们需要支持更多复杂的语句,比如条件判断和循环;这里我们先写一个带循环的模板文件:
var template =
'My skills:' +
'<%for(var index in this.skills) {%>' +
'<a href=""><%this.skills[index]%></a>' +
'<%}%>';
var skills= ["js", "html", "css"]
如果使用字符串拼接的话,代码就应该是下面的样子:
var temolate='My skills:' +
for(var index in this.skills) { +
'<a href="">' +
this.skills[index] +
'</a>' +
}
这里会产生一个异常,Uncaught SyntaxError: Unexpected token for。如果我们调试一下,把code变量打印出来,我们就能发现问题所在。
var Arr=[];
Arr.push("My skills:");
Arr.push(for(var index in this.skills) {);
Arr.push("<a href=\"\">");
Arr.push(this.skills[index]);
Arr.push("</a>");
Arr.push(});
Arr.push("");
return Arr.join("");
带有for循环的那一行不应该被直接放到数组里面,而是应该作为脚本的一部分直接运行。所以我们在把内容添加到code变量之前还要多做一个判断。
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var Arr=[];\n',
cursor = 0;
var add = function(line, js) {
js? code += line.match(reExp) ? line + '\n' : 'Arr.push(' + line + ');\n' :
code += 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n';
}
这里我们新增加了一个正则表达式。它会判断代码中是否包含if、for、else等等关键字。如果有的话就直接添加到脚本代码中去,否则就添加到数组中去。运行结果如下:
var Arr=[];
Arr.push("My skills:");
for(var index in this.skills) {
Arr.push("<a href=\"\">");
Arr.push(this.skills[index]);
Arr.push("</a>");
}
Arr.push("");
return Arr.join("");
来看下最终代码:
var TemplateEngine = function(html, options) {
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var Arr=[];\n',
cursor = 0;
var add = function(line, js) {
js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
(code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
while(match = re.exec(html)) {
add(html.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(html.substr(cursor, html.length - cursor));
code += 'return r.join("");';
return new Function(code.replace(/[\r\t\n]/g, ''));
}
当然你也可以在模板文件中加入更复杂的逻辑比如判断等;
3. 现在来实现以下我们开头说的列表效果:
var songs =[
{name:'刚刚好', singer:'薛之谦', url:'http://music.163.com/xxx'},
{name:'最佳歌手', singer:'许嵩', url:'http://music.163.com/xxx'},
{name:'初学者', singer:'薛之谦', url:'http://music.163.com/xxx'},
{name:'绅士', singer:'薛之谦', url:'http://music.163.com/xxx'},
{name:'我们', singer:'陈伟霆', url:'http://music.163.com/xxx'},
{name:'画风', singer:'后弦', url:'http://music.163.com/xxx'},
{name:'We Are One', singer:'郁可唯', url:'http://music.163.com/xxx'}
]
var html =
'<div class="song-list">'+
' <h1>热歌榜</h1>'+
' <ol>'+
'<%for(var i=0; i<this.songs.length;i++){%>'+
'<li><%this.songs[i].name%> - <%this.songs[i].singer%></li>'+
'<%}%>'+
' </ol>'+
'</div>'
模板文件和数据
var TemplateEngine = function(html,options) {
var re = /<%([^%>]+)?%>/g,
reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
code = 'var Arr=[];\n',
cursor = 0;
var add = function(line, js) {
js? (code += line.match(reExp) ? line + '\n' : 'Arr.push(' + line + ');\n') :
(code += line != '' ? 'Arr.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
while(match = re.exec(html)) {
add(html.slice(cursor, match.index))
add(match[1], true);
cursor = match.index + match[0].length;
}
add(html.substr(cursor, html.length - cursor));
code += 'return Arr.join("");';
console.log(code);
var result= new Function (code.replace(/[\r\t\n]/g, ''))
return result
}
模板引擎
最后吧返回结果渲染到页面上
var results =TemplateEngine (html,songs)
document.body.innerHTML = results();
最终完成一个自己的模板引擎效果,实现了用户界面与业务数据(内容)分离。当然你可以根据你的需求加上更多的逻辑比如判断条件语句等等.
如果你每步都跟着做下来即理解了模板引擎的原理,也可以自己做出一个自己的模板引擎,希望对给位朋友有所帮助~~~
版权归饥人谷--楠柒所有如有转发请注明出处谢谢~~