一、一个简单的需求
用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
-
[ object Arguments ] {
0 : "{0}" ,
1 : "0" ,
2 : 4 ,
3 : "<li>{0} - {1}</li>"
} -
[ object Arguments ] {
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/