本文引至: please call me HR
js的模板匹配是页面渲染很重要的一块. 无论是后端的同构,还是前端ajax拉取. 如果数据复杂, 那么使用js模板引擎将会是一个非常方便的工具. 常用的就有arTemplate, mustache.js等. 一个具体的特征表示符就是:<%= %>
和<% %>
. 当然,还有mustache的{{ }}
. 不过,这里我们先不谈这些虚的, 我们来实现一个简单的模板引擎.
解析原理
首先,模板引擎的工作就是,将你的template转化为实际的HTML。 更具体来说,就是将template转化为string.
// template
<% for(var i in items){ %>
- <%= items[i].text %>
<% } %>
// 实际转化为
var temp = '';
for(var i in items){
temp += "- " + items[i].text + "
";
}
temp += '
';
上面想表达的意思就是,如何将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 = '';
for(var i in items){
temp += "- " + items[i] + "
";
}
temp += '
';
return temp;
`
var render = new Function("items",str);
console.log(render([1,2,3]));
// 返回
// - 1
- 2
- 3
另外一个就是,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 = '';
for(var i in items){
temp += "- " + items[i] + "
";
}
temp += '
';
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 = `
<% for(var i in items){ %>
- <%= items[i] %>
<% } %>
`;
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) { +
'' +
this.skills[index] +
'' +
}
// 编译
var r = [];
r.push('My skills:');
for(var index in this.skills) {
r.push('');
r.push(this.skills[index]);
r.push('');
}
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) { +
'' +
this.skills[index] +
'' +
}
// 在部署时候进行编译,把template 经由引擎自动生成一个函数
function preCompile(){
var r = [];
r.push('My skills:');
for(var index in data.skills) {
r.push('');
r.push(data.skills[index]);
r.push('');
}
return new Function('data',r.join(''));
}
坊间传闻,artTemplate使用预编译的模板来和其他的模板引擎做比较,然后证明他的性能高超... 俺artTemplate比mustache快xxx倍, 牛逼么?
看到这里, 我就呵呵了一句. 亲, 您用到引擎了吗? 你顶多使用了函数...
但,不得不承认,artTemplate能够想到使用precompile,并且做的很棒,这是值的肯定的. 后来,TmosJS的出现,让前端模板可以使用模块化进行组合(比如,include). 到这里,模板引擎这块已经到了一个峰值了. 后面的难点就是如何进行模板的更新和替换了.