文首的话 这篇文章总结和自己尝试实现一下模板。然后总结下业界当前各种先进的模板技术。 算是一种锻炼吧,写了那么多业务,也用那么多轮子。自己来造轮子看看。
字符串模板的分析 模板也有很多种,但是这里仅仅分析和实现基于字符串的模板。为了方便,这里就基于artTemplate的语法来分析模板应该具备怎样的功能。
字符串中变量解析
条件表达式
遍历输出
字符串中变量解析 先来弄第一个最简单功能,变量替换: 目标是实现下面代码:
1 2 3 4 5 6 7 8 var data = { name:"张三" , age:"19" }; var tplString = "{{name}}今年{{age}}岁" ;var string = tpl(tplString,data);console .log(string);
下面是个人实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function tpl(str,data){ //定义抽出变量正则 var valReg = /[\{]{2}\s*\w+\s*[\}]{2}/g; //获取字符串数组 var strArr = str.match(valReg); //获取变量数组 var valArr = strArr.join("-").replace(/[\{]{2}\s*/g,"").replace(/\s*[\}]{2}/g,"").split("-"); for(var i=0,len=strArr.length;i<len;i++){ str=str.replace(strArr[i],data[valArr[i]]); } return str; }
整个函数很简单,只是简单的使用replace对变量进行了简单的替换。匹配和替换过程中对花括号之间的变量进行了去空格处理。 这个函数还比较粗糙,比如,它仅仅可以处理第一层的数据,假如使用zhangsan.name就会失败。这里先放放。我们继续看条件表达式的处理。
条件表达式 效果大概是这样的:
1 2 3 4 5 6 7 {{if admin}} <p>admin</p> {{else if code > 0 }} <p>master</p> {{else }} <p>error!</p> {{/if }}
发现这个做起来挺麻烦的。主要要点:
抽出多行模板文本
模板文本转换成函数
函数求值
抽出多行表达式 首先要抽出从if开始到else到/if之间的文本。暂时写个可以用的正则匹配一下,不太严格:
1 2 3 var blockIfReg = /[\{]{2}\s*if[\s\S]*[\{]{2}\s*\/if[\}]{2}/mg; //匹配出来的文本是这样的: //["{{if admin>0}}\n <p>admin</p>\n {{else if code > 0}}\n <p>master</p>\n {{else}}\n <p>error!</p>\n {{/if}}"]
其中↵代表的是换行符号。现在问题是如何优雅的把这个字符串转换为表达式了。。。
先来把这个替换gt什么的符号还原一下,使用replace做这个替换:
1 2 3 4 5 6 7 8 9 10 11 var mapObj = { ">" :">" , "<" :"<" }; var matchStringArr = s.match(blockIfReg)||[]; for (var a in mapObj){ matchStringArr = matchStringArr.map(function (v ) { return v.replace(new RegExp (a,'gm' ),mapObj[a]); }) }
其中s是匹配的字符串数组,重点是replace里面的new RegExp(a,’gm’),这里使用了正则,需要替换所有的——顺便发现”字符串中变量解析”时候没有发现这个。回头补下bug。
继续写个正则抽一抽,主要目的是把逻辑字符串抽出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 HTML: <div id ="tpl" > {{if admin>0}} <p > admin</p > {{else if code < 0}} <p > master</p > {{else}} <p > error!</p > {{/if}} </div > JS: var s = document.getElementById("tpl"); console.log(s.split(/[\{]{2}\s*|\s*[\}]{2}/gm)); ["", "if admin>0", "↵admin↵↵", "else if code > 0", "↵master↵↵", "else", "↵error!↵↵", "/if", ""]
这里干完以后发现有Bug:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="tpl" > {{if admin>0}} <p > admin</p > {{else if code < 0}} <p > master</p > {{else}} <p > error!</p > {{/if}} {{if aaa>0}} <p > aaa</p > {{else if bbbb < 0}} <p > master</p > {{else}} <p > error!</p > {{/if}} </div >
此时有两个if结构了,但是/[{]{2}\s*if[\s\S]*[{]{2}\s*/if[}]{2}/mg
匹配成了一个,加个?干掉贪婪匹配:/[{]{2}\s*if[\s\S]*?[{]{2}\s*/if[}]{2}/mg
现在可以了。可以获取匹配到两个包含字符串组成的数组.
下面继续干活:目标是将字符串转换为函数
先将字符串切一切:
1 2 3 matchStringArr=matchStringArr.map(function (v ) {return v.split(/[\{]{2}\s*|\s*[\}]{2}/gm )}) console .log(matchStringArr);
结果如图:
继续往下处理一下,将其转换成可以用的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 matchStringArr=matchStringArr.map(function(v){ return transform(v); }); function transform(v){ var targetString = ""; v.forEach(function(val,i){ if(val==""){ }else if(/^if/.test(val)){ targetString+="if("+ val.split(" ")[1]+"){"+v[i+1]+"}"; }else if(val.indexOf("else if")>-1){ targetString+="else if("+val.replace("else if","")+"){"+v[i+1]+"}" }else if(val.indexOf("else") > -1){ targetString+="else{"+v[i+1]+"}" } }); return targetString; }
console.log之:
到这里基本就差不多了。当然,现在这个函数就算执行了也没卵用,不过先不管,先将其弄成函数再说。究竟。。。有问题会报错的,到时候解决。 这里可以使用eval和Function->eval不建议使用,使用Function好了。
字符串转换函数 Function Function方式构建函数平时几乎没有机会做。不过这里来尝试一下。
1 2 3 4 5 6 7 8 9 var sayAge = Function ("a" ,"b" ,"return a+'的年龄是'+b" );console .log(sayAge("张三" ,"15" ))var sayAge = Function ("a" ,"return a['name']+'的年龄是'+a['age']" );console .log(sayAge({name :"李四" ,age :"18" }));
这样没啥问题了。完善下上面的东西。
整理下之前弄出来的函数字符串:
1 2 3 4 5 6 7 if (admin>0 ){ <p>admin</p> }else if (code < 0 ){ <p>master</p> }else { <p>error!</p> }
这是一个条件表达式,若干逻辑中取一个返回,先修一修让它可以正常工作 然后使用Function构建一个函数试试,这里要测试一下使用Function构建一下改造过的函数是否可以运行 这里是代码运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 var str = "if(admin>0){return '<p>admin</p>'}else if(code < 0){return '<p>master</p>'}else{return '<p>error!</p>'}" ;var testFun = Function ("admin" ,"code" ,str);console .log(testFun(1 ,1 ));console .log(testFun(-1 ,-1 ));
运行效果良好
这里为了上面代码正常工作改良一下原来的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function transform (v ) { var targetString = "" ; v.forEach(function (val,i ) { if (val=="" ){ }else if (/^if/ .test(val)){ targetString+="if(" + val.split(" " )[1 ]+"){return '" +v[i+1 ].replace(/^\s+|\s+$/g ,"" )+"'}" ; }else if (val.indexOf("else if" )>-1 ){ targetString+="else if(" +val.replace("else if" ,"" )+"){return '" +v[i+1 ].replace(/^\s+|\s+$/g ,"" )+"'}" }else if (val.indexOf("else" ) > -1 ){ targetString+="else{return '" +v[i+1 ].replace(/^\s+|\s+$/g ,"" )+"'}" } }); return targetString; }
这里添加了return,同时把无用的换空格干掉了,同时\s顺便把\n也换掉了。这样,上面的函数就可以很好的运行了。
Function内部变量处理 到这里逻辑基本算是好了,现在面临的问题是,如何直接传入对象,让函数里面的表达式可以获取到而不需要加this[“admin”]什么。个人的想法是将该变量放到Function构造出的函数的上一层即可。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function test (obj ) { for (var a in obj){ this [a] = obj[a]; } return { getString:function ( ) { var str = "if(admin>0){return '<p>admin</p>'}else if(code < 0){return '<p>master</p>'}else{return '<p>error!</p>'}" ; var insertFn = new Function ('' ,str); return insertFn.apply(this ,null ); } }; }
JS Bin on jsbin.com
还算成功,可以顺便把之前无法读zhangsan.name这种问题统统解决掉。
包装修缮一下门面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var str = "if(admin>0){return '<p>admin</p>'}else if(code < 0){return '<p>master</p>'}else{return '<p>error!</p>'}" ;function getTplChunk (obj,fnString ) { function tmp (obj,fnString ) { for (var a in obj){ this [a] = obj[a]; } return { getString:function ( ) { var insertFn = new Function ('' ,fnString); return insertFn.apply(this ,null ); } }; }; var target = tmp(obj,fnString).getString(); tmp=null ; return target; } var obj = { admin:1 , code:1 }; console .log(getTplChunk(obj,str));
demo:JS Bin on jsbin.com
至此上面两个要点就OK了。
话外之With 这里使用了闭包和作用域链来实现了代码不加前缀实现变量暴露。除此以外,一个看似更可能更好的实现这个功能的,是with语句。with可以创建一个作用域,在作用域内,在引用特定对象属性时候可以不使用前缀。这是几乎更加契合场景的实现。 但是《JavaScript:The Good Parts》作者已经说过”with Statement considered Harmful”,并且ES5的use strict已经禁用了它。那么就放弃使用它吧。反正。。。使用作用域链,我也把它实现了。。。
变量解析和表达式整理 至此前两个逻辑已经OK,但是两个逻辑之间有些碎片化。我们整理一下代码思路进行重构:
模板函数接受元素id和数据进行内部操作
内部操作环节对模板字符串进行分解
依次调用不同方法进行替换并返回最终字符串
目前只整理了字符串和表达式的处理方式,下面是,整理好的代码,直接放到jsbin了,贴到博文太长不方便看。JS Bin on jsbin.com
这里需要说的是,逻辑先处理了表达式再处理了字符串,主要是考虑到这个会通字符串匹配冲突。同时之前发现的若干很明显的bug都修好了。
遍历表达式 遍历表达式的模板语法是这样的:
1 2 3 {{each list as value index}} <li>{{index}} - {{value.user}}</li> {{/each}}
首先,要把它抽取出来,这里使用正则抽取它。/[{]{2}each\s.*[}]{2}[\s\S]*[{]{2}/each[}]{2}/gm
,找个了HTML页面测试通过。
接下来要把他换算成表达式。like this:
1 2 3 4 var target = "" list.forEach(function(value,index){ target += '<li>{{index}} - {{value.user}}</li>'; })
直接改造下transform函数好了,给它加个type参数分类处理。 然后获取下列关注点的变量字符串
正则提取: /[{]{2}each\s+(\w+)\s+as\s+(\w+)\s+(\w+)*[}]{2}([\s\S]*)[{]{2}/each[}]{2}/gm
改造的transform分支2:
1 2 3 4 5 6 7 }else if(type===2){ var reg = /[\{]{2}each\s+(\w+)\s+as\s+(\w+)\s+(\w+)*[\}]{2}([\s\S]*)[\{]{2}\/each[\}]{2}/gm; v = reg.exec(v); targetString = 'var a ="";'+v[1]+'.forEach(function('+v[2]+','+v[3]+'){' + 'a += "<li>{{index}} - {{value.user}}</li>"' +'});return a;'; }
现在问题是li里面的变量不解析,那么我们复用一下字符串解析函数好了,之前保留了data参数可以传参进去。 现在是这样:
1 2 3 4 5 6 7 8 9 }else if(type===2){ var reg = /[\{]{2}each\s+(\w+)\s+as\s+(\w+)\s+(\w+)*[\}]{2}([\s\S]*)[\{]{2}\/each[\}]{2}/gm; v = reg.exec(v); targetString = targetString += 'var a ="";'+v[1]+'.forEach(function('+v[2]+','+v[3]+'){' + 'a += '+'fn1("'+v[4].replace(/\s/g,"")+'",'+v[2]+')' +'});return a;'; }
此时问题是fn1获取不到,改下getTplChunk:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function getTplChunk(obj,fnString){ function tmp(obj,fnString){ var isFunction = (Object.prototype.toString.call(fnString)==="[object Function]"); for(var a in obj){ this[a] = obj[a]; }; this["fn1"]=fn1; return { getString:function(){ var insertFn = isFunction?fnString:(new Function('',fnString)); return insertFn.apply(this,null); } }; } var target = tmp(obj,fnString).getString(); tmp=null; //销毁闭包 return target; }
将this[“fn1”]=fn1;假如到作用域上边去; 此时console出来的值:
1 <li>undefined-{{value.user}}</li><li>undefined-{{value.user}}</li>
存在两个问题:
index索引不在作用域链上
加了点号的变量无法解析&value.user的方式和上文的不太一样。
第2点如此处理:
改下正则匹配: var valReg = /[{]{2}\s*\w+[.\w+]*\s*[}]{2}/g;
这样可以获取花括号里面的东西。
使用正则干掉前面的前缀引用,这样可以复用之前的逻辑。
1 2 3 targetString += 'var a ="";' +v[1 ]+'.forEach(function(' +v[2 ]+',' +v[3 ]+'){' + 'a += ' +'fn1("' +v[4 ].replace(/\s/g ,"" ).replace(new RegExp (v[2 ]+'.' ,"gm" ),"" )+'",' +v[2 ]+')' +'});return a;' ;
第1点参照第2点加变量的方式,可以同样处理,由于index是动态的,那么就要动态传参到fn1了。不过fn1本身会接受一个data,我们加到里面去就ok了。但是在这里上下文不是很好理清,同时,一个变量最终解析时候到底是变量,还是字符串,必须要小心处理。
现在是处理后的:
1 2 3 4 5 6 7 8 }else if (type===2 ){ var reg = /[\{]{2}each\s+(\w+)\s+as\s+(\w+)\s+(\w+)*[\}]{2}([\s\S]*)[\{]{2}\/each[\}]{2}/gm ; v = reg.exec(v); targetString += 'var a ="";' +v[1 ]+'.forEach(function(' +v[2 ]+',' +v[3 ]+'){' + 'a += ' +'fn1("' +v[4 ].replace(/\s/g ,"" ).replace(new RegExp (v[2 ]+'.' ,"gm" ),"" )+'",addAttr(' + v[2 ] +',{"' +v[3 ]+'":' +v[3 ]+'}))' +'});return a;' ; }
——我加了一个addAttr函数到getTplChunk了。
1 2 3 4 5 6 function addAttr (target,obj ) { for (var a in obj){ target[a] = obj[a]; } return target; }
至此模板的工作初步完工了,考虑到字符串渲染如果排在each渲染之前,那么会导致它破坏了each里面字符串,因此我们把fn3放在最前面工作。我们来个Demo渲染包含三种表达式的模板看看。
JS Bin on jsbin.com
至此,模板就算编写完毕了。现在它还存在一个比较明显的bug:
有空的话我在补起来,主要是正则需要完善。
总结 这里就实现了不太完善的类似artTemplate的模板——全程没有看过artTemplate源码哦。 这里说下自己对基于字符串模板的体会:
简单:删掉注释和空行不压缩101行,随便两个花括号放一行就能到100行了,最核心的是三个replace就OK了。
复杂:
当逻辑从字符串抽出时候需要依赖大量相对复杂的正则
with存在性能低下和被ES5严格模式废弃问题,需要使用其他方式规避
使用相对偏门的Function构造函数(不是eval)和构造函数的上下文,以及如何进行变量传递等
其他模板技术&&碎碎念 本来想写一写其他模板技术的观望的,发现一篇资料,大家可以前往观望。 文首有一句话是:
此文的写作耗时很长,称之为雄文不为过,小心慢用
个人深以为然。说是雄文确实不为过。
前端组件之争,好像有些陷入模板之争。
最早的时候,Backbone让我们从满是jQuery选择器的沼泽里面拖出来。 然后是AngularJS,双向绑定让我们知道了原来操作DOM可以不用去手工获取和修改节点。 然后是React,组件化的特性,让我们知道了,应用可以像积木一样拼起来,有时候HTML都可以不用写了。
MVC,M和C是如此稳定没有见到玩出太多花样,而面向用户的V层发展到越来越深。
innerHTML如此流行,我以为这是IE干过最积极的事情。Backbone这类MVC框架依赖模板来将数据转换为HTML字符串,然后使用innerHTML来数据插入。innerHTML是如此快速,以至于大面积的DOM操作几乎没有别的选择;但是它又是如此慢,大面积的模板渲染里面即使只进行最小粒度的更新,它也会全部更新一次,顺便——撸掉你绑定的事件。
AngularJS如果视为模板,有些不合适,因为它做了模板以外的事情(事实上,我一直认为,只有基于字符串的模板,才能算是纯正的模板)。但是它确实是如此好用——如果你精熟而非遇到问题束手无策的普通用户的话。 AngularJS在双向绑定方案上使用了脏检测方案,每个双向绑定的元素会存在一个watcher,当触发检测时候会一个个进行对比,然后进行View或者Model的更新。显然,这种方式让优雅和性能洁癖的工程师会有看法。
至于React,我个人认为它最大的贡献除了是小粒度的innerHTML,还有组件化开发方式。——webComponent,感觉入前端以来,从未有一个概念如此让人心往神驰。
好了,不碎嘴了。大家看文章去:
点此前往: 一个对前端模板技术的全面总结