当前位置: 首页 > 科技观察

前端模板的原理与实现

时间:2023-03-22 15:06:00 科技观察

现在流行什么,比如react、avalon、angular、vue等等,其核心都离不开前端模板。理解前端模板是理解MV*的关键。前端框架最重要的目的是渲染页面。“渲染”这个词最初并不是前端的东西。前端以前叫抠图,就是把设计师做的PSD变成静态页面,然后加入动态交互。但是我们后台的数据很多,怎么给静态页面添加数据呢?所以有一个额外的过程叫做“设置页面”。设置页面的过程其实就是把静态页面切割成一块一块的,每一块都是一个php、jsp或者vm文件,它们是后端模板引擎的处理对象!事实上,模板并不局限于后端或前端,模板的本质是一种完成从数据(变量)到实际可视化表示(HTML代码)的工作的手段。由于后端临水而台先得月(取数据更方便),所以先在后端开发这项技术。这些后端模板文件是活跃在服务器上的,然后经过复杂的处理,最终被浏览器渲染出来。此时的渲染就是将服务端拼接的静态文本变成DOM树的过程。如果你想实现前端MVC或MVP,那些流程必须改变。静态文件输出不变,尤其是大公司,分工够细,有专门的裁剪组(女生居多)制作。然后是页面集。这个时候就不能使用后端模板引擎了,需要引入前端模板引擎。因为实现一个前端模板引擎太简单了,经过多年的发展,已经有了很多有用的轮子:https://github.com/janl/musta...JavaScript-basedLogic-less(没有逻辑或者光逻辑)模板。https://github.com/twitter/ho...上面的优化版本,twitter出品https://github.com/wycats/han...完全兼容mustcachehttps://github的语法。com/paularmstr。..有更强大的模板继承和块重写功能https://github.com/mozilla/nu...类似于django的模板系统,可以说是swig的升级版,是gitbook的女王前端模板等推荐还有ejs,好学好用。对于有ASP/PHP/JSP编程经验的人来说,是非常友好和自然的。缺点是功能有点简单。其他doT、xtempalate、Underscore模板。最不推荐的是jade,有点华而不实,设计过度,导致页面集合工作量大,性能差。虚拟DOM时代流行的jsx是没有逻辑的模板。无逻辑或者轻逻辑模板流行的主要原因是修改成本比较低。像玉一样自制的语法糖太多了,从美工那里拿来的html需要开战,经过令人心碎的改造才能设置数据。.对于模板来说,最简单的说就是把某个变量数据放在一个合适的地方(填空),其次你可以控制这个区域输出或者不输入(如果有指令),或者允许多次循环输入的区域(用于指令),更具强制性,实现模板的嵌套(布局和块)。if和for的实现方式有两种,一种是简单的区域,插入一个js语句,里面包含if语句和for语句,另一种是使用语法糖,比如ms-for,ms-repeat,ng-如果,重复。使用语法糖比直接使用JS语句简单,但是带来了学习成本和扩展功能。每个模板的if、for指令的语法都不一样,如果要在循环中做一些处理,比如过滤一些数据,或者突然断掉某处,就得引用一些新的语句。由于模板需要前后共享,因此存在传输成本。直接在模板中写JS语句,绝对不是语法糖。因此,由于这些原因,小胡子风格的模板成为了主流。现在有三种模板样式:PHP/ASP/JSP样式:<%if(list.length){%>

    <%for(n=0;n
  1. <%=list[n]%>
  2. <%}%>
<%}%>mustcache样式,高级语法有限,通常难以自定义和扩展:{{#iflist.length}}
    {{#eachlistitem}}
  1. {{item}}
  2. {{/each}}
{{/if}}属性绑定样式:{{item}}前两个只能出现在script和textarea等容器元素内部。因此,<分隔符与标签的<容器冲突,不利于IDE的格式化处理。属性绑定风格是MVVM时代最流行的模板定义风格。页面的某个区域就是模板,不需要进行追加等操作。下面我们看看如何实现前端模板。前端模板的本质是一个可以转换函数(渲染函数)的字符串。在渲染函数放入一个充满数据的对象后,它恢复为一个全新的字符串。所以重点是如何构建渲染函数。最简单的方法是正则化。还记得第2章的格式化方法吗?这是一种用于填充数据的轻量级方法。functionformat(str,object){vararray=Array.prototype.slice.call(arguments,1);returnstr.replace(/\\?\#{([^{}]+)\}/gm,function(匹配,name){if(match.charAt(0)=='\\')returnmatch.slice(1);varindex=Number(name)if(index>=0)returnarray[index];if(object&&object[name]!==void0)returnobject[name];return'';});}格式化方法是用#{}来划分静态内容和动态内容,一般称为定界符(delimiter)。#{是前定界符,}是后定界符,这个#{}其实是ruby风格的定界符。通常的分隔符是<%和%>、{{和}}。通常在前面的定界符中有一些修饰符,比如=号,表示这会输出到页面,还有-号,表示去掉两边的空格。将下面的例子编译成渲染函数vartpl='嗨,我叫<%name%>,今年<%info.age%>岁'vardata={name:"司徒正妹",age:20}就可以了大概是这样的varbody='你好,我叫'+data.name+',今年是'+data.info.age+'岁'varrender=newFunction('data','return'+body)orBesmart并使用数组加入:vararray=['return']array.push('Hi,mynameis')array.push(data.name)array.push(',alreadythisyear')array.push(data.info.age)array.push('old')varrender=newFunction('data',array.join('+'))需要区分静态内容和给变量加上数据前缀..这一步可以做使用正则表达式或纯字符串。让我们试试纯字符串方法。假设前面的分隔符是openTag,后面的分隔符是closeTag,那么可以通过indexOf和slice方法来切块。functiontokenize(str){varopenTag='<%'varcloseTag='%>'varret=[]do{varindex=str.indexOf(openTag)index=index===-1?str.length:indexvarvalue=str.slice(0,index)//提取{{前面的静态内容ret.push({expr:value,type:'text'})//改变str字符串本身str=str.slice(index+openTag.length)if(str){index=str.indexOf(closeTag)varvalue=str.slice(0,index)//提取{{和}}的动态内容ret.push({expr:value.trim(),//两侧的JS逻辑空格可以省略type:'js'})//改变str字符串本身str=str.slice(index+closeTag.length)}}while(str.length)returnret}console.log(tokenize(tpl))然后通过render方法将它们拼接在一起。functionrender(str){vartokens=tokenize(str)varret=[]for(vari=0,token;token=tokens[i++];){if(token.type==='text'){ret.push('"'+token.expr+'"')}else{ret.push(token.expr)}}console.log("return"+ret.join('+'))}打印出return"你好,我的名字是“+name+”,今年是“+info.age+”岁“这个方法不完整。首先,只在两边加上双引号是不靠谱的,如果里面有双引号怎么办。所以需要引入第2章介绍的quote方法,当type为text时,ret.push(+quote(token.expr)+)。其次,您需要将.data添加到动态部分的变量中。你怎么知道它是一个变量。让我们回忆一下变量的定义,它是以_、$或字母开头的字符组合。为了简洁起见,我们暂时不需要说中文。但是,在字符串info.age中,其实有两个匹配变量的子串,我只需要添加数据即可。在信息前面。这时候我们就需要在匹配变量之前,尝试把对象的子层属性替换成不匹配变量的字符,然后用go替换。为此,我做了一个挖填的方法,将子级别的属性改成类似??12这样的字符串:return"Hello,mynameis"+name+",今年是"+info.age+"岁“输出为return”你好,我叫“+data.name+”,我是“+data.info.age+”今年岁“***,我们修改下两行,得到我们梦寐以求的渲染函数,它的实现过程比format方法复杂的多,但是是所有高扩展前端模板的通用实现过程。functionrender(str){//略。..returnnewFunction("data","re??turn"+ret.join('+'))}varfn=render(tpl)console.log(fn+"")console.log(fn(data))我们来看看如何引入一个循环语句,比如把上面的模板和数据改成vartpl='你好,我叫<%name%>,今年是<%info.age%>岁,比如<%for(vari=0,el;el=list[i++];){%><%el%><%}%>'vardata={name:"司徒正妹",info:{age:20},list:["apple","banana","Sydney"]}这时候我们会添加一个新的类型,它不会输出到页面的动态内容中。这对令牌方法进行了一些修改value=value.trim()if(/^(if|for|})/.test(value)){ret.push({expr:value,type:'logic'})}else{ret.push({expr:value,type:'js'})}但是怎么修改render方法,说明此时不能再继续用+了,否则就结束了像这样return"Hello,mynameCalled"+data.name+",今年是"+data.info.age+"岁,like"+for(vari=0,el;el=list[i++];){+""+data.el+""+}这个时候我们需要借用数组,把要输入的数据(文本,js类型)放进去,逻辑类型不放进去。functionaddPrefix(str){//先去掉对象的子级属性,减少干扰因素varjs=str.replace(rproperty,dig)js=js.replace(ident,function(a){return'data.'+a})returnjs.replace(rfill,fill)}functionaddView(s){return'__data__.push('+s+')'}functionrender(str){stringPool={}vartokens=tokenize(str)varret=['var__data__=[]']tokens.forEach(function(token){if(token.type==='text'){ret.push(addView(quote(token.expr)))}elseif(token.type==='logic'){//逻辑部分由addPrefix方法处理ret.push(addPrefix(token.expr))}else{ret.push(addView(addPrefix(token.expr))}})ret.push("return__data__.join('')")console.log(ret.join('\n'))}varfn=render(tpl)得到的内部结构是这样的,显然addPrefix方法有问题,我们应该过滤掉if、for等关键字和保留字。var__data__=[]__data__.push("你好,我的名字是")__data__.push(data.name)__data__.push(",今年已经")__data__.push(data.info.age)__data__.push("我老了,喜欢")data.for(data.vardata.i=0,data.el;data.el=data.list[data.i++]){__data__.push("")__data__.push(data.el)__data__.push("")}return__data__.join('')但是就算去掉关键字和保留字,中间生成的i和el怎么区分呢?他们分不清。所以目前有两种方法,一种是用with,那我们就不用再添加数据了。字首。第二个引入了新的语法。比如前面的@换成了data..先看第一个:functionrender(str){stringPool={}vartokens=tokenize(str)varret=['var__data__=[];','with(data){']for(vari=0,token;token=tokens[i++];){if(token.type==='text'){ret.push(addView(quote(token.expr)))}elseif(token.type==='logic'){ret.push(token.expr)}else{ret.push(addView(token.expr))}}ret.push('}')ret.push('return__data__.join("")')returnnewFunction("data",ret.join('\n'))}varfn=render(tpl)console.log(fn+"")console.log(fn(data))许多迷你模板与一起制造,以减少更换工作。第二种方法,使用引导符@,是avalon2的玩法。这个addPrefix方法可以减少很多代码。相应的,模板也要改成vartpl='你好,我叫<%@name%>,今年<%@info.age%>岁,我喜欢<%for(vari=0,el;el=@list[i++];){%><%el%><%}%>'varrguide=/(^|[^\w\u00c0-\uFFFF_])(@|##)(?=[$\w])/gfunctionaddPrefix(str){returnstr.replace(rguide,'$1data.')}functionrender(str){stringPool={}vartokens=tokenize(str)varret=['var__data__=[];']for(vari=0,token;token=tokens[i++];){if(token.type==='text'){ret.push(addView(quote(token.expr)))}elseif(token.type==='logic'){//逻辑部分由addPrefix方法处理ret.push(addPrefix(token.expr))}else{ret.push(addView(addPrefix(token.expr)))}}ret.push('return__data__.join("")')returnnewFunction("data",ret.join('\n'))}varfn=render(tpl)console.log(fn+"")console.log(fn(数据))第二种相对于第一种的优势是性能更高,并且避免了es5严格模式的限制。我们再考虑一下。事实上,循环语句和条件语句不仅是for和if,还有while、dowhile和else。所以这个需要优化。还有两种方法可以添加更多的语法符号。比如上面说的=就是输出,没有输出就不输出。这是ASP/JSP/PHP等模板采用的方法://tokenizemethodif(value.charAt(0)==='='){ret.push({expr:value,type:'js'})}else{ret.push({expr:value,type:'logic'})}另一个,使用语法糖,比如@list中的#each(el,index),'#eachEnd','#if','#ifEnd'。或者更改标记化方法if(value.charAt(0)==='#'){if(value==='#eachEnd'||value==='#ifEnd'){ret.push({expr:'}',type:'logic'})}elseif(value.slice(0,4)==='#if'){ret.push({expr:'if('+value.slice(4)+'){',type:'logic'})}elseif(value.slice(0,6)==='#each'){vararr=value.slice(6).split('in')vararrayName=arr[1]varargs=arr[0].match(/[$\w_]+/g)varitemName=args.pop()varindexName=args.pop()||'$index'value=['for(var','=0;','<'+arrayName+'.length;','++){'].join(indexName)+'\nvar'+itemName+'='+arrayName+'['+indexName+'];'ret.push({expr:value,type:'logic'})}}else{//...}对应的模板改为vartpl='你好,我叫<%@name%>,今年有已经<%@info.age%>岁了,比如<%#eachelin@list%><%el%><%#eachEnd%>'varfn=render(tpl)console.log(fn+"")console.log(fn(data))可能有人会觉得#for,#forEnd这样的语法糖很丑,没问题,这个是可以改的,主要是我们的tokenize方法足够强大,可以实现像mustache这样的模板引擎。但是所有的模板引擎基本都是这样实现的,有的还支持过滤器,就是对js类型的语句进行处理,把|后面的字符设备截掉。虚拟DOM呢?然后你需要一个html解析器,这是一个巨大的工程。比如reactive库,早期没有使用html解析器和虚拟DOM,只有三四千行。加上这些炫酷的功能后,达到了1W6K行。返回字符串与返回对象树结构(如DOM树)不同。