“
React、Vue、Angular等均属于MVVM模式,在一些只需完成数据和模板简单渲染的场合,显得笨重且学习成本较高,而解决该问题非常优秀框架之一是doT.js,本文将对它进行详解。
”
前端渲染有很多框架,而且形式和内容在不断发生变化。这些演变的背后是设计模式的变化,而归根到底是功能划分逻辑的演变:MVC—>MVP—>MVVM(忽略最早混在一起的写法,那不称为模式)。近几年兴起的React、Vue、Angular等框架都属于MVVM模式,能帮我们实现界面渲染、事件绑定、路由分发等复杂功能。但在一些只需完成数据和模板简单渲染的场合,它们就显得笨重而且学习成本较高了。
例如,在美团外卖的开发实践中,前端经常从后端接口取得长串的数据,这些数据拥有相同的样式模板,前端需要将这些数据在同一个样式模板上做重复渲染操作。
解决这个问题的模板引擎有很多,doT.js(出自女程序员Laura Doktorova之手)是其中非常优秀的一个。下表将doT.js与其他同类引擎做了对比:
可以看出,doT.js表现突出。而且,它的性能也很优秀,本人在Mac Pro上的用Chrome浏览器(版本为:56.0.2924.87)上做100条数据10000次渲染性能测试,结果如下:
从上可以看出doT.js更值得推荐,它的主要优势在于:
本文主要对doT.js的源码进行分析,探究一下这类模板引擎的实现原理。
如果之前用过doT.js,可以跳过此小节,doT.js使用示例如下:
可以看出doT.js的设计思路:将数据注入到预置的视图模板中渲染,返回HTML代码段,从而得到最终视图。
下面是一些常用语法表达式对照表:
和后端渲染不同,doT.js的渲染完全交由前端来进行,这样做主要有以下好处:
-
脱离后端渲染语言,不需要依赖后端项目的启动,从而降低了开发耦合度、提升开发效率;
-
View层渲染逻辑全在JavaScript层实现,容易维护和修改;
-
数据通过接口得到,无需考虑后端数据模型变化,只需关心数据格式。
doT.js源码核心:
...
// 去掉所有制表符、空格、换行
str = ("var out='" + (c.strip ? str.replace(/(^|\r|\n)\t* +| +\t*(\r|\n|$)/g," ")
.replace(/\r|\n|\t|\/\*[\s\S]*?\*\//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+"
这段代码总结起来就是一句话:用正则表达式匹配预置模板中的语法规则,将其转换、拼接为可执行HTML代码,作为可执行语句,通过new Function()创建的新方法返回。
代码解析重点1:正则替换
正则替换是doT.js的核心设计思路,本文不对正则表达式做扩充讲解,仅分析doT.js的设计思路。先来看一下doT.js中用到的正则:
templateSettings: {
evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g, //表达式
interpolate: /\{\{=([\s\S]+?)\}\}/g, // 插入的变量
encode: /\{\{!([\s\S]+?)\}\}/g, // 在这里{{!不是用来做判断,而是对里面的代码做编码
use: /\{\{#([\s\S]+?)\}\}/g,
useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$\.]+|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,// 自定义模式
defineParams:/^\s*([\w$]+):([\s\S]+)/, // 自定义参数
conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g, // 条件判断
iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\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:new Function()运用
函数定义时,一般通过Function关键字,并指定一个函数名,用以调用。在JavaScript中,函数也是对象,可以通过函数对象(Function Object)来创建。正如数组对象对应的类型是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执行字符串代码,new Function()恰好解决了这个问题。回过头来看doT代码中的”new Function(c.varname, str)”,就不难理解varname是传入可执行字符串str的变量。
具体关于new Fcuntion的定义和用法,详细请阅读
Function详细介绍
。