自己实现一个模板引擎

一、一个简单的需求

用js渲染歌曲列表,并且要求不能写死,数据来自一个songs数组。

<div class=song-list>
  <h1>热歌榜</h1>
  <ol>
    <li>刚刚好 - 薛之谦</li>
    <li>最佳歌手 - 许嵩</li>
    <li>初学者 - 薛之谦</li>
    <li>绅士 - 薛之谦</li>
    <li>我门 - 陈伟霆</li>
    <li>画风 - 后弦</li>
    <li>We Are One - 郁可唯</li>
  </ol>
</div>
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'}
]

 

可以想到最笨的两种方法:

1、html字符串拼接

var html = '';
html +='<div class=song-list>';
html +='  <h1>热歌榜</h1>';
html +='  <ol>';
for (var i=0; i<songs.length; i++){
  html +='<li>'+songs[i].name+'-'+songs[i].singer+'</li>';
}
html +='</ol>';
html +='</div>';

document.body.innerHTML = html;

2、构造DOM对象

var elDiv = document.createElement('div');
elDiv.className = 'song-list';
var elH1 = document.createElement('h1');
elH1.appendChild(document.createTextNode('热歌榜'));
var elOl = document.createElement('ol');
for (var i=0; i<songs.length; i++){
  var elLi = document.createElement('li');
  elLi.textContent = songs[i].name + '-' +songs[i].singer;
  elOl.appendChild(elLi);
}
elDiv.appendChild(elH1);
elDiv.appendChild(elOl);

document.body.appendChild(elDiv);
//利用jquery

    var $Div = $('<div class="song-list"></div>');
    var $H1 = $('<h1>热歌榜</h1>');
    var $Ol = $('<ol></ol>');

    for (var i=0; i<songs.length; i++){
      var $Li = $('<li></li>');
      $Li.text(songs[i].name + '-' +songs[i].singer);
      $Ol.append($Li);
      
    }
    $Div.append($H1);
    $Div.append($Ol);

    $('body').append($Div);

我们可以发现这种方式比较繁琐,而且容易出现错误,那有没有方法是可以简化的呢?这时候就创造出了模板引擎的玩意。首先来看看我们的需求

将如下字符串拼接

var li = '<li>' + songs[i].name + ' - ' + songs[i].singer + '</li>'

变成

var li = stringFormat('<li>{0} - {1}</li>', songs[i].name, songs[i].singer)

那我们就可以利用stringFormat函数来格式化字符串,这个函数接收第一个参数是string,其中{0}代表后传入的第一个变量,依次类推

实现这个函数你需要懂得基础的正则表达式和str.replace('',function(){})用法。下面先来看看replace('',function(){})的使用

function stringFormat(str){
  var params = [].slice.call(arguments,1);
  var regex =/\{(\d+)\}/g;
  str = str.replace(regex,function(){
    console.log(arguments);
  })
}

var tpl = '<li>{0} - {1}</li>';
var result = stringFormat(tpl,'刚刚好','薛之谦');

//console
  • [objectArguments]{
     
    0:"{0}",
     
    1:"0",
     
    2:4,
     
    3:"<li>{0} - {1}</li>"
    }
  • [objectArguments]{
     
    0:"{1}",
     
    1:"1",
     
    2:10,
     
    3:"<li>{0} - {1}</li>"
    }

我们将replace里的第二个参数函数打印出arguments,可以发现传入的四个参数依次是1、匹配到的需要替换的值,2、正则的分组值,3、匹配到的值在字符串中的位置,4、字符串。接下来我们来完善stringFormat函数

function stringFormat(str){
  //第一点是传入的参数是不确定的,需要利用arguments来取到
  //第二点是我们需要的是arguments的除第一个的参数,可以用到数组的方法slice,但是arguments是类数组对象,所以可以用call来改变函数的执行上下文
  //params是一个参数数组
  var params = [].slice.call(arguments,1);
  //然后我们需要找到{数字}这样格式的字符串,然后将它替换
  var regex =/\{(\d+)\}/g;
  //接下来我们就可以替换了,但是怎么知道找到索引值呢?
  //{数字},传入的字符串里面的{}里的数字就是对应params数组中的索引,所以我们要想办法把它取出来,可以利用正则中的分组将数字单独取出来
  //接下来可以利用replace的函数arguments[1]取出索引值。
  str = str.replace(regex,function(){
    var index = arguments[1];
    return params[index];
  })
  return str;
}

var tpl = '<li>{0} - {1}</li>';
var result = stringFormat(tpl,'刚刚好','薛之谦');
console.log(result);  //<li>刚刚好 - 薛之谦</li>

这样几行代码简单的模板引擎就实现了,但是这样还不够,我们想要功能更强大的模板函数。需求如下:

var string = 
    '<div class=song-list>' +
    '  <h1>热歌榜</h1>' +
    '  <ol>'
        <%for (var i=0; i<songs.length; i++) {%>
        <li><%songs[i].name%> - <%songs[i].singer%></li>
        <%}%>
      </ol>' +
    '</div>'

var data = {
    songs: songs
}

var result = template(string, data)

document.body.innerHTML = result

在升级版的模板引擎中我们只需要传入template(string,data) 字符串和数据。我们也分步实现,首先实现变量的替换,需求如下;

var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
var data = {
    name: "Krasimir",
    age: 29
}
console.log(TemplateEngine(template, data)); 

实现变量替换我们需要的仍然是replace和正则的知识,这次用到一个新的api:regex.exec()它的作用是每次只做一次匹配,如果正则表达式没有g,即没有全局匹配,每次调用这个方法时,都是匹配到第一个。如果正则表达式有g,则每次依次匹配,直到没有匹配的返回null,但如果再继续匹配则从第一个匹配项开始循环匹配。

var str ='good,better,wonderful,good';
var reg = /good/;
reg.exec(); //[good]
reg.exec(); //[good]
reg.exec(); //[good]
reg.exec(); //[good]
reg.exec(); //[good]
reg.exec(); //[good]


//全局匹配
var str ='good,better,wonderful,good';
var reg = /good/;
reg.exec(); //[good]  
reg.exec(); //[good]
reg.exec(); //null
reg.exec(); //[good]  
reg.exec(); //[good]
reg.exec(); //null

接下来我们来写出这个变量替换的函数

var TemplateEngine = function(tpl, data) {
    var regex = /<%([^%>]+)?%>/g;
    while(match = regex.exec(tpl)) {
      //将匹配到的与传入的数据进行替换
        tpl = tpl.replace(match[0], data[match[1]]);
    }
    return tpl;
} 
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
var data = {
    name: "Krasimir",
    age: 29
}
console.log(TemplateEngine(template, data));

可以看到最重要的是会写正则表达式,并且能够灵活利用。平时可以利用编辑器多练习,sublime中ctrl+h是replace的快捷键。

但是我们会发现如果传入的数据是有嵌套的对象形式,就会出现问题

{
    name: "luoqian",
    profile: { age: 29 }
}

这样你就无法replace为data['profile'],这时候我们想如果能直接运行js代码就好了,如下

var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';

大神John rege使用了new Function的语法,根据字符串创建一个函数。平时大家用到的都函数表达式和函数声明的方法,下面来看看new Function的用法是怎么样的

 

var fn = new Function('arg','console.log(arg+1)');
fn(3) //4

 

可以看出,创建了一个fn函数,第一个参数为函数的参数,第二个参数为函数体。虽然用这种方法能够解决这样的问题。

var data = {
  name: "Krasimir", age: 29
}
function fn() {
  return '<p>Hello, my name is ' +this.name+ ' I\'m ' + this.age + ' years old.</p>';
}
fn.call(data); //"<p>Hello, my name is Krasimir I'm 29 years old.</p>"

除此之外,实际的模板引擎中,我们会把模板切分为小段的文本和又意义的js代码。比如一些循环语句。

var template = 
'My skills:' + 
'<%for(var index in this.skills) {%>' + 
'<a href=""><%this.skills[index]%></a>' +
'<%}%>';

这个代码不能直接使用,我们可以把所有的字符串都放在一个数组里,在程序最后把它们拼接起来。

var r = [];
r.push('My skills:');
for (var i in this.skills) {
r.push('<a href="">');
r.push(this.skills[i]);
r.push('</a>');
}
return r.join('');
}

有了这些思路就可以尝试写出复杂版的模板引擎函数

var TemplateEngine = function(html, options) {
    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\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, '')).apply(options);
}

测试函数

var template = 
'My skills:' + 
'<%if(this.showSkills) {%>' +
    '<%for(var index in this.skills) {%>' + 
    '<a href="#"><%this.skills[index]%></a>' +
    '<%}%>' +
'<%} else {%>' +
    '<p>none</p>' +
'<%}%>';
console.log(TemplateEngine(template, {
    skills: ["js", "html", "css"],
    showSkills: true
}));

这样我们就不用再傻逼的拼接字符串了,TemplateEngine()函数传入两个参数模板1、字符串2、数据

我们在写模板的时候就可以直接用js只需要在语句和变量时加上特殊的标示符<%%>。

 

参考文章:http://blog.jobbole.com/56689/

 

posted @ 2017-02-15 23:14  罗Q  阅读(2131)  评论(0编辑  收藏  举报