VillainHR

模板引擎原理&实例

模板引擎 es6 2016-08-24

js的模板匹配是页面渲染很重要的一块. 无论是后端的同构,还是前端ajax拉取. 如果数据复杂, 那么使用js模板引擎将会是一个非常方便的工具. 常用的就有arTemplate, mustache.js等. 一个具体的特征表示符就是:<%= %><% %>. 当然,还有mustache的{{ }}. 不过,这里我们先不谈这些虚的, 我们来实现一个简单的模板引擎.

解析原理

首先,模板引擎的工作就是,将你的template转化为实际的HTML。 更具体来说,就是将template转化为string.

// template
<ul>
	<% for(var i in items){ %>
		<li class='<%= items[i].status %>'><%= items[i].text %></li>
	<% } %>
</ul>

// 实际转化为
var temp = '<ul>';
for(var i in items){
	temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
}
temp += '</ul>';

上面想表达的意思就是,如何将template 合理的转化为一个string. 这里, 我们主要针对<%= %><% %>来进行讲解. 这个简单的引擎主要涉及到两个只是点,一个是new Funciton(){},还有一个是replace.

new Funciton()

一般我们定义一个函数, 最快捷的办法就是

function a(param){
    body...
}
// 或者
var a = function(param){
    body...
}

//很少有
var a = new Function(param,body);
// 并且body里面只能是string类型. 

不过,我们这里就是使用这个body的string类型来完成字符串的解析. 看个实例:

var str = `var temp = '<ul>';
for(var i in items){
	temp += "<li>" + items[i] + "</li>";
}
temp += '</ul>';
return temp;
`
var render = new Function("items",str);
console.log(render([1,2,3]));
// 返回
// <ul><li>1</li><li>2</li><li>3</li></ul>

另外一个就是,replace.

replace

因为获取的是一个字符串.所以,我们通常需要使用正则来进行简单的匹配. 最先想到的就是match. 但是,他是一次性输入结果,不能在循环当中,进行字符串的获取. 这里,就需要使用到replace这个方法. 他有一个内在的feature.即, 如果你使用正则的global模式,他会执行全部匹配和替换.基本格式为:

str.replace(regexp|substr, newSubStr|function)

主要看一下后面带函数的内容:

function(match,p1,..pn,offset,string){}
  • match: 表示匹配到的字符串. 不管怎样都要进行返回. 这样才能保证最终的字符串完整.
  • p1…pn: 这是正则分组的结果.根据你的()来确定,你有多少个选项.
  • offset: 当前匹配字符在整个字符中的起始位置.相当于indexOf(xx)返回的内容.
  • string: 原始字符串

这里,需要说明一点, replace后面的function并不是只会执行一次,他会执行多次.因为,他是按照正则匹配到的顺序执行的(执行的是惰性匹配) 看一个简单的demo:

function replacer(match, p1, p2, offset, string) {
  if(match){
  	return 2;
  }
  return match;
}
// 将匹配到的内容,全部换为2.
var newString = 'abc12345#$*%'.replace(/(\d+)|([^\w]*)/g, replacer);
console.log(newString);
// 返回 abc22

这应该就算是比较简单的了. 接下来,我们来正式的看一看模板引擎具体的流程.

解析流程

我们这里主要是针对<% %><%= %>. 这里,先放出两个正则匹配:

var evaluate = /<%([\s\S]+?)%>/; // <% %>
var interpolate = /<%=([\s\S]+?)%>/; // <%= xx %>

有童鞋,可能会疑惑为什么变量名会是这两个. 实际上,这是ERB模板原理提出的两个基本概念. 相当于就是,一个是变量替代,一个是直接渲染而已. 关键点其实并不在这, 而是在如果将一个template拼接为一个function_body. 这md才是真难. 还记得上面的格式是:

var temp = '<ul>';
for(var i in items){
	temp += "<li>" + items[i] + "</li>";
}
temp += '</ul>';
return temp;

简单的说就是, 将<% %>直接拼接+=,之后又是temp+=即可. 而<%= %>则直接是变量名的渲染. 写一下伪代码就是:

if(interpolate){
    function_body+="';"+interpolate+"temp+='";
}
if(evalute){
    function_body+="'"+evalute+"'";
}

结合replace 中回调function 内容, 这里直接将正则匹配写为优先级.

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
// 后面的$ 是用来匹配最后截断的字符串.

在匹配的时候,需要注意,将\r\n这个给escaper掉,不然,后面出bug都不知道是怎么弄出来的. 因为正则有时候是不会给你做这个工作的.转义也很简单.直接将\r变为\\r即可. 因为在实际的render中,浏览器会自动识别的.我们这里主要是让他在第一次compile时,将换行给去掉.

var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
text = text.replace(escaper,'');

react在处理这个JSX的时候,也是使用这种方式,将所有的换行符全部给escape掉. 则总的代码为:

var str = `
<ul>
	<% for(var i in items){ %>
		<li><%= items[i] %></li>
	<% } %>
</ul>
`;

var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g

//模板文本中的特殊字符转义处理
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;

//text: 传入的模板文本字串
//data: 数据对象
var template = function(text,data){
	var index = 0;//记录当前扫描到哪里了
	text = text.replace(escaper,'');
	var function_body = "var temp = '';";
	function_body += "temp += '";
	text.replace(matcher,function(match,interpolate,evaluate,offset){
		//找到第一个匹配后,将前面部分作为普通字符串拼接的表达式
		//添加了处理转义字符
		function_body += text.slice(index,offset);
			// .replace(escaper, function(match) { return '\\' + escapes[match]; });

		//如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组
		if(evaluate){
			function_body += "';" + evaluate + "temp += '";
		}
		//如果是<%= ... %>拼接字符串,interpolate就是捕获的分组
		if(interpolate){
			function_body += "' + " + interpolate + " + '";
		}
		//递增index,跳过evaluate或者interpolate
		index = offset + match.length;
		//这里的return没有什么意义,因为关键不是替换text,而是构建function_body
		return match;
	});
		//最后的代码应该是返回temp
	function_body += "';return temp;";
	var render = new Function('items', function_body);
	return render(data);
}
console.log(template(str,[1,2,3]));

上面这种方法,应该是较一般的方法渲染的快一点, 因为他只涉及到字符串的拼接和调用Function的渲染函数. 不过, 这里我还是要祭出jquery作者,John Resig写的Micro-Templating的方法.

push方式-John

John写的方式,应该算是大部分模板共同使用的一种方式. 采用先拼接后渲染.

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

    return data ? fn(data) : fn;
}

简单的来说就是:

// 原始模板
'My skills:' + 
for(var index in this.skills) { +
'<a href="">' + 
this.skills[index] +
'</a>' +
}
// 编译
var r = [];
r.push('My skills:'); 
for(var index in this.skills) {
r.push('<a href="">');
r.push(this.skills[index]);
r.push('</a>');
}
return r.join('');

通过push操作,将指定HTML插入,并且加上数据渲染. 这种方式,实际上和上面的差别就在于拼接这一块. 使用push进行拼接的,要比使用+=拼接的慢4倍左右. 不过,这在单次渲染过程中,并没有什么太大的影响. 再次声明上面两种方式是非常初级和不安全的. 因为没做任何的escape,并且在性能上也是有点欠缺的. 现在比较流行的模板引擎主要有: mustache.js,artTemplate. mustache.js是以他独有的语法格式, like: {{#name}},{{/name}} 来实现 for,if等逻辑判断的. 他相对于以前的ERB引擎来说, 速度快,语法简洁(但也难学…) 然后就是artTemplate, artTemplate 较其他引擎比起来就比较快了. 或者,我们也可以仅仅把他叫做模板,因为,他可以实现预编译(precompile). 即,将引擎在浏览器中做的那一部分,挪到开发者自动编译环节. 这里,我们来简单说一下预编译.

预编译

什么叫做预编译呢? 这估计看到这个名词,有点bigger的感觉. 但实际上, 他做的工作,就是上面我们写的两个引擎做的事, 他通过gulp或者webpack自动实现编译函数的生成和合并.即:

// 原始template
'My skills:' + 
for(var index in this.skills) { +
'<a href="">' + 
this.skills[index] +
'</a>' +
}

// 在部署时候进行编译,把template 经由引擎自动生成一个函数
function preCompile(){
    var r = [];
    r.push('My skills:'); 
    for(var index in data.skills) {
    r.push('<a href="">');
    r.push(data.skills[index]);
    r.push('</a>');
    }
    return new Function('data',r.join(''));
}

坊间传闻,artTemplate使用预编译的模板来和其他的模板引擎做比较,然后证明他的性能高超… 俺artTemplate比mustache快xxx倍, 牛逼么? 看到这里, 我就呵呵了一句. 亲, 您用到引擎了吗? 你顶多使用了函数… 但,不得不承认,artTemplate能够想到使用precompile,并且做的很棒,这是值的肯定的. 后来,TmosJS的出现,让前端模板可以使用模块化进行组合(比如,include). 到这里,模板引擎这块已经到了一个峰值了. 后面的难点就是如何进行模板的更新和替换了.

原文链接: https://www.villianhr.com/2016/08/24/模板引擎原理&实例