五分钟了解模板引擎原理,阅读后做出自己的模板引擎

1.什么是模板引擎

  • 概念:模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档。

  • 原理:就我个人的理解模板的诞生是为了将显示与数据分离,其本质是将模板文件和数据通过模板引擎生成最终的HTML代码(如图)

QQ截图20170409162847.jpg

模板引擎的基本机理就是替换(转换),将指定的标签转换为需要的业务数据;将指定的伪语句按照某种流程来变换输出。比如要实现如下的页面要求:
1.数据来自一个数组 songs;
2.不能写死在页面里

QQ截图20170409163610.jpg

遍历拼接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);
QQ截图20170409164848.jpg

我们来一行一行的解释下代码:

 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;
全局匹配以<%开头,中间不是%或>并以%>结尾的配配项,(如图)

QQ截图20170409170957.jpg

如图所示:如果我们选中了匹配后的数组的第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();
QQ截图20170411224137.jpg

github代码地址

最终完成一个自己的模板引擎效果,实现了用户界面与业务数据(内容)分离。当然你可以根据你的需求加上更多的逻辑比如判断条件语句等等.

如果你每步都跟着做下来即理解了模板引擎的原理,也可以自己做出一个自己的模板引擎,希望对给位朋友有所帮助~~~

版权归饥人谷--楠柒所有如有转发请注明出处谢谢~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容