js渲染html-后端渲染引擎doT.js分析

2023-09-01 0 7,085 百度已收录

背景

后端渲染的框架有很多,但是方法和内容在不断变化。 这种演变的背后是设计模式的变化,归根到底是功能定义逻辑的演变:MVC->MVP->MVVM(忽略最早的混合写法,不叫模式)。 近年来流行的React、Vue、Angular等框架都属于MVVM模型,可以帮助我们实现界面渲染、事件绑定、路由分发等复杂功能。 但在某些只需要完成数据和模板的简单渲染的场合,就显得比较繁琐,学习成本较高。

例如,在美团订餐的开发实践中,后端经常从前端接口获取长串数据。 这些数据具有相同的样式模板,后端需要在相同的样式模板上对这些数据进行重复的渲染操作。

有很多模板引擎可以解决这个问题,doT.js(由女程序员 Laura Doktorova 创建)是最好的之一。 下表将 doT.js 与其他类似引擎进行了比较:

|框架|大小|压缩版大小|迭代次数|条件表达式|自定义句型||————|————|————|————||doT.js|6KB|4KB |✓ |✓|✓||小胡子|18.9KB|9.3KB|✓|✗|✓||车把|512KB|62.3KB|✓|✓|✓||artTemplate (腾讯)|-|5.2KB|✓|✓ |✓ ||百度模板(百度)|9.45KB|6KB|✓|✓|✓||jQuery-tmpl|18.6KB|5.98KB|✓|✓|✓|

可以看出,doT.js 表现出色。 而且,它的性能也非常出色。 我在MacPro上使用Chrome浏览器(版本:56.0.2924.87)做了100条数据、10000次渲染性能测试。 结果如下:

性能测试

从上面可以看出,doT.js 更值得推荐。 其主要优点是: 1、紧凑精简js渲染html,源代码不超过两百行,大小6KB,压缩版只有4KB; 2.支持丰富的表达方式,包含几乎所有应用场景的表达语句; 3、性能优良; 4.不依赖第三方库。

本文主要分析doT.js的源码,探讨该类模板引擎的实现原理。

如何使用

如果您之前使用过 doT.js,则可以跳过本节。 使用doT.js的示例如下:

<script type="text/html" id="tpl">
    <div>
        <a>name:{{= it.name}}</a>
        <p>age:{{= it.age}}</p>
        <p>hello:{{= it.sayHello() }}</p>
        <select>
            {{~ it.arr:item}}
                <option {{?item.id == it.stringParams2}}selected{{?}} value="{{=item.id}}">
                    {{=item.text}}
                </option>
            {{~}}
        </select>
    </div>
</script>
<script>
    $("#app").html(doT.template($("#tpl").html())({
        name:'stringParams1',
        stringParams1:'stringParams1_value',
        stringParams2:1,
        arr:[{id:0,text:'val1'},{id:1,text:'val2'}],
        sayHello:function () {
            return this[this.name]
        }
    }));
</script>

可见doT.js的设计思想:将数据注入到预设的视图模板中进行渲染,并返回HTML代码段,从而得到最终的视图。

以下是一些常用句子表达方式的对照表:

项目JavaScript句型对应句型案例

输出变量

js渲染html-后端渲染引擎doT.js分析

{{=变量名}}

{{=it.name}}

条件判断

如果

{{?条件表达式}}

{{?i>3}}

条件转移

否则/elseif

{{??i==2}}

依次通过

为了

{{~循环变量}}

{{~it.arr:项目}}…{{~}}

实施方式

函数名()

{{=函数名称()}}

js渲染html-后端渲染引擎doT.js分析

{{=它。 问好()}}

源码分析及实现原理

与前端渲染不同的是,doT.js 的渲染完全交给了后端。 这样做有以下主要好处:

与前端渲染语言分离,不需要依赖前端项目的启动,从而增加开发耦合,提高开发效率; View层渲染逻辑全部在JavaScript层实现,易于维护和更改; 通过socket获取数据,不需要考虑前端数据模型的改变,只需要关心数据格式。

doT.js源码核心:

...
// 去掉所有制表符、空格、换行
str = ("var out='" + (c.strip ? str.replace(/(^|r|n)t* +| +t*(r|n|$)/g," ")
         .replace(/r|n|t|/*[sS]*?*//g,""): str)
   .replace(/'|\/g, "\$&")
   .replace(c.interpolate || skip, function(m, code) {
      return cse.start + unescape(code,c.canReturnNull) + cse.end;
   })
   .replace(c.encode || skip, function(m, code) {
      needhtmlencode = true;
      return cse.startencode + unescape(code,c.canReturnNull) + cse.end;
   })
   // 条件判断正则匹配,包括if和else判断
   .replace(c.conditional || skip, function(m, elsecase, code) {
      return elsecase ?
         (code ? "';}else if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}else{out+='") :
         (code ? "';if(" + unescape(code,c.canReturnNull) + "){out+='" : "';}out+='");
   })
   // 循环遍历正则匹配
   .replace(c.iterate || skip, function(m, iterate, vname, iname) {
      if (!iterate) return "';} } out+='";
      sid+=1; indv=iname || "i"+sid; iterate=unescape(iterate);
      return "';var arr"+sid+"="+iterate+";if(arr"+sid+"){var "+vname+","+indv+"=-1,l"+sid+"=arr"+sid+".length-1;while("+indv+"<l"+sid+"){"
         +vname+"=arr"+sid+"["+indv+"+=1];out+='";
   })
   // 可执行代码匹配
   .replace(c.evaluate || skip, function(m, code) {
      return "';" + unescape(code,c.canReturnNull) + "out+='";
   })
   + "';return out;")
 ...
try {
    return new Function(c.varname, str);//c.varname 定义的是new Function()返回的函数的参数名
  } catch (e) {
    /* istanbul ignore else */
     if (typeof console !== "undefined") console.log("Could not create a template function: " + str);
     throw e;
  }
...

这段代码可以用一句话来概括:使用正则表达式匹配预设模板中的句型规则,转换拼接成可执行的HTML代码,并通过newFunction()创建的新方法将其作为可执行句子返回。

代码分析重点1:定期替换

定期替换是doT.js的核心设计思想。 本文不对正则表达式进行展开,仅分析doT.js的设计思想。 我们首先看一下doT.js中使用的常规规则:

templateSettings: {
   evaluate:    /{{([sS]+?(}?)+)}}/g, //表达式
   interpolate: /{{=([sS]+?)}}/g, // 插入的变量
   encode:      /{{!([sS]+?)}}/g, // 在这里{{!不是用来做判断,而是对里面的代码做编码
   use:         /{{#([sS]+?)}}/g,
   useParams:   /(^|[^w$])def(?:.|[['"])([w$.]+)(?:['"]])?s*:s*([w$.]+|"[^"]+"|'[^']+'|{[^}]+})/g,
   define:      /{{##s*([w.$]+)s*(:|=)([sS]+?)#}}/g,// 自定义模式
   defineParams:/^s*([w$]+):([sS]+)/, // 自定义参数
   conditional: /{{?(?)?s*([sS]*?)s*}}/g, // 条件判断
   iterate:     /{{~s*(?:}}|([sS]+?)s*:s*([w$]+)s*(?::s*([w$]+))?s*}})/g, // 遍历
   varname:   "it", // 默认变量名
   strip:    true,
   append:       true,
   selfcontained: false,
   doNotSkipEncoded: false // 是否跳过一些特殊字符
}

正则的定义在源码中一起提及,方便维护和管理。 在早期版本的doT.js中,处理条件表达式的形式与tmpl相同,采用直接替换为可执行语句的方法。 在最新版本的doT.js中,只需将其改为一条正则规则即可实现替换,这样更容易发简洁。

doT.js源码中,模板中正则替换句型的流程如下:

渲染过程

代码分析重点2:newFunction()应用

定义函数时,通常使用Function关键字并指定调用的函数名。 在JavaScript中,函数也是对象,可以通过函数对象(FunctionObject)来创建函数。 正如链表对象对应的类型是Array,日期对象对应的类型是Date,如下所示:

var funcName = new Function(p1,p2,...,pn,body);

参数的数据类型都是字符串。 p1到pn表示创建的函数的参数名称列表,body表示创建的函数的函数体语句,funcName是创建的函数的名称(可以创建匿名函数,不指定任何参数)。

下面的定义是等效的。

例如:

// 一般函数定义方式
function func1(a,b){
    return a+b;
}
// 参数是一个字符串通过逗号分隔
var func2 = new Function('a,b','return a+b');
// 参数是多个字符串
var func3 = new Function('a','b','return a+b');
// 一样的调用方式
console.log(func1(1,2));
console.log(func2(2,3));
console.log(func3(1,3));
// 输出
3 // func1
5 // func2
4 // func3

从里面的代码可以看出,Function的最后一个参数被转换成了可执行代码,和eval的函数类似。 执行eval时,会出现浏览器性能提升、调试困难、可能出现XSS(跨站)攻击等问题。 因此,不建议使用eval来执行字符串代码。 newFunction() 正好解决了这个问题。 回顾一下doT代码中的“newFunction(c.varname,str)”,不难理解varname就是可执行字符串str中传入的变量。

newFcuntion的详细定义和使用请阅读Function的详细介绍。

性能原因

读完本文,你可能会有疑问:为什么doT.js在众多引擎中表现如此出色? 通过阅读其他引擎的源码,我发现它们的核心代码段都存在各种问题。

jQuery-tmpl

function buildTmplFn( markup ) {
        return new Function("jQuery","$item",
            // Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
            "var $=jQuery,call,__=[],$data=$item.data;" +
            // Introduce the data as local variables using with(){}
            "with($data){__.push('" +
            // Convert the template into pure JavaScript
            jQuery.trim(markup)
                .replace( /([\'])/g, "\$1" )
                .replace( /[rtn]/g, " " )
                .replace( /${([^}]*)}/g, "{{= $1}}" )
                .replace( /{{(/?)(w+|.)(?:(((?:[^}]|}(?!}))*?)?))?(?:s+(.*?)?)?((((?:[^}]|}(?!}))*?)))?s*}}/g,
                function( all, slash, type, fnargs, target, parens, args ) {
                    //省略部分模板替换语句,若要阅读全部代码请访问:https://github.com/BorisMoore/jquery-tmpl
                }) +
            "');}return __;"
        );
    }

从前面的代码中可以看出,jQuery-teml同样采用了newFunction()的形式来编译模板,而在性能对比中,jQuery-teml的性能远远落后于doT.js。 性能困境的关键在于with语句。 使用。

为什么with语句对性能影响这么大? 我们看下面的代码:

var datas = {persons:['李明','小红','赵四','王五','张三','孙行者','马婆子'],gifts:['平民','巫师','狼','猎人','先知']};
function go(){
    with(datas){
        var personIndex = 0,giftIndex = 0,i=100000;
        while(i){
            personIndex = Math.floor(Math.random()*persons.length);
            giftIndex = Math.floor(Math.random()*gifts.length)
            console.log(persons[personIndex] +'得到了新的身份:'+ gifts[giftIndex]);
            i--;
        }
    }
}

里面的代码中使用了with表达式。 为了防止多次从数据中取出变量,使用了with语句。 这看似提高了效率,但却产生了性能问题:当 JavaScript 中执行方法时,会形成执行上下文。 该执行上下文保存了方法的作用域链,主要用于标识符解析。 当代码流到达 with 表达式时,运行时上下文的作用域链会暂时更改,并创建一个新的可变对象,其中包含指定对象的所有属性。 该对象被插入到作用域链的末尾,这意味着函数的所有局部变量现在都被拖到第二个作用域链对象中。 这样,访问数据的属性非常快,但是访问局部变量就变得很慢。 ,所以接入成本较高,如右图所示。

当这个插件在 GitHub 上推出时,作者 Boris Moore 主要关注两个设计思路:

模板缓存,重复使用模板时,直接使用显存中缓存的模板。 在本文作者看来,这是一个无用的功能。 在实际使用中,无论是直接用String编写的模板,还是从Dom中获取的模板,都会以变量的形式存储在显存中。 如果变量使用得当,它将出现在页面上。 该模板在整个生命周期内可用。 经过源码分析,发现jQuery-tmpl的模板缓存并没有缓存模板编译结果,在多次执行渲染时会导致多次编译。 再加上代码的性能消耗,严重拖慢了整个渲染过程。 Template标签,可以从缓存模板中检索对应的子节点。 这是一个很好的设计思想js渲染html,可以实现数据变化时只重新渲染部分界面的功能。 而且我认为:模板将渲染结果交给开发者并渲染到界面指定位置后,模板引擎的工作就应该结束了,剩下的节点操作就应该由开发者灵活控制了。

在不改变原有设计思路的情况下,尽量提高源代码的性能。

我们保留改进前的性能进行比较:

性能改进

首先我们来做第一个性能提升,把源码中的with字样去掉。

第一次提升后:

绩效提升2

拿第二部分来改进和实现BorisMoore设计理念中的模板缓存:

绩效提升3

这部分优化代码段被我们修改为:

function buildTmplFn( markup ) {
    if(!compledStr){
        // Convert the template into pure JavaScript
        compledStr = jQuery.trim(markup)
            .replace( /([\'])/g, "\$1" )
            .replace( /[rtn]/g, " " )
            .replace( /${([^}]*)}/g, "{{= $1}}" )
            .replace( /{{(/?)(w+|.)(?:(((?:[^}]|}(?!}))*?)?))?(?:s+(.*?)?)?((((?:[^}]|}(?!}))*?)))?s*}}/g,
                //省略部分模板替换语句
    }
    return new Function("jQuery","$item",
        // Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
        "var $=jQuery,call,__=[],$data=$item.data;" +
        // Introduce the data as local variables using with(){}
        "__.push('" + compledStr +
        "');return __;"
    )
}

doT.js源码中没有像with这样的消耗性能的语句。 同时,doT.js选择先将模板编译结果返回给开发者,这样如果多次使用同一个模板进行渲染,就不会重复编译。 。

仅 25 行的模板:tmpl

(function(){
  var cache = {};
  this.tmpl =  function (str, data){
    var fn = !/W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +
        "with(obj){p.push('" +
        str
          .replace(/[rtn]/g, " ")
          .split("<%").join("t")
          .replace(/((^|%>)[^t]*)'/g, "$1r")
          .replace(/t=(.*?)%>/g, "',$1,'")
          .split("t").join("');")
          .split("%>").join("p.push('")
          .split("r").join("\'")
      + "');}return p.join('');");
    return data ? fn( data ) : fn;
  };
})();

读完这段代码,你会惊讶地发现它更像是简化版的baiduTemplate。 与baiduTemplate相比,它不仅去掉了baiduTemplate自定义句子标签的功能,使代码更加精简,而且避免了更换用户句子标签带来的性能消耗。 对于 doT.js 来说,性能问题的关键在于 with 语句。

基于以上,我把tmpl源码中的with语句去掉:

改造前性能:

tmpl 性能改进

改造后性能:

tmpl 性能提高 2

如果读者对性能对比源码感兴趣,可以访问。

总结

通过分析doT.js源码,我们发现:

doT.js的条件判断语句标签并不直观。 当开发者在使用过程中嵌套过多的条件判断时,很难找到对应的结束句符号。 开发者需要严格规范代码编写,否则会造成开发和维护的困难。 doT.js 限制开发者自定义句子标签。 相比之下,baiduTemplate提供了可定制标签的功能。 baiduTemplate的性能困境恰恰在于提供自定义句子标签的功能。

很多解决我们问题的插件的代码往往简单明了,但是这些庞大的插件都有副作用或者无用的功能。 技术领域有一种软件设计范式:“约定小于配置”,它减少了软件开发人员需要做出的决策数量,变得简单灵活。 在插件编译过程中,开发者应该更加注重使用场景和性能的有机结合,使用合适的句型,尽可能减少开发者的配置,不要追求取悦每个场景。

关于作者

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 html js渲染html-后端渲染引擎doT.js分析 https://www.wkzy.net/game/186518.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务