从使用方法分析
先来看一个例子,了解一下什么是模版引擎:
var str = "hello: <%=name %>"
var parese = _.template(str)
console.log(parese({name: '麦乐'})) // hello: 麦乐
再看一下复杂点的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
#root {
height: 2000px;
width: 100%;
}
</style>
<body>
<div id="root"></div>
<script type="text/html" id="tmpl">
<ul>
<%for ( var i = 0; i < list.length; i++ ) { %>
<li>
<a href="<%=list[i].url%>">
<%=list[i].name%>
</a>
</li>
<% } %>
</ul>
</script>
</body>
<script src="underScore.js"></script>
<script>
var root = document.getElementById('root')
var list = [ { "name": "百度", "url": "https://www.baidu.com" },
{ "name": "Daisy", "url": "https://www.baidu.com" },
{ "name": "Kelly", "url": "https://www.baidu.com" }]
var parse = _.template(document.getElementById('tmpl').innerHTML)
const html = parse({list})
root.innerHTML = html
</script>
</html>
var template = _.template("<b><%- value %></b>");
var html = template({value: '<div>'});
root.innerHTML = html
模版引擎分三种情况:
- <% ... %> 任意的js代码
- <%= ... %> 插入变量
- <%- ... %> html转译\
模版引擎其实就是一段字符串,怎么把字符串转换为可一执行的js代码呢?想这里,大家应该都能想到eval了。
eval
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
一般模式
我们就需要将诸如此类的"<b><%= value %></b>"字符串,拼接成js代码字符串,然后再使用eval使其执行,是不是就ok了呢?
var value = "麦乐"
eval("var _p = '<b> '+ value+' </b>'")
console.log(_p) // <b> 麦乐 </b>
严格模式
function test (params) {
'use strict'
var value = "麦乐"
eval("var _p = '<b> '+ value+' </b>'")
console.log(_p) // <b> 麦乐 </b>
}
test()
eval确实可以实现,但是eval在严格模式是无法使用的,所以这里使用eval肯定不行的,下面介绍另外一种方式:
Function
一般模式
看下这个函数的使用方法:
var adder = new Function("a", "b", "return a + b");
adder(2, 6); // 8
var adder = new Function("obj", "return obj.url");
console.log(adder({url: "httpd://www.baiducom"})) // httpd://www.baiducom
new Function()参数都是字符串,但是最后一个字符串参数会被当作js代码来编译。
严格模式
function test (params) {
'use strict'
var value = "麦乐"
var adder = new Function("value", "var _p = '<b> '+ value+' </b>'; return _p");
console.log(adder(value)) // <b> 麦乐 </b>
}
test()
可以看到严格模式下也支持。
所以如果可以将"<b><%= value %></b>"这样的字符串,转化为"var _p = '<b> '+ value+' </b>'; return _p"这样的字符串,就能满足需求了。
模版字符串拆分
第一步:匹配内部
先使用正则找到<% %>内部的内容。
举例:要或者"<b><%= value %></b>"内部内容,需要写一个正则来实现:
var str = "<b><%= value %></b>"
var reg = /<%=([\s\S]+?)%>/g
str.replace(reg, function (match, $1, offset) {
console.log(match, $1, offset) // <%= value %>(匹配到的内容) value(括号内部捕获到的内容) 3(开始匹配的起始位置)
})
这里只处理一种情况,<%= %>,可以拿到想要的内容,而实际不只有这一种情况,还有<%- %> 和<% %>这两种情况,所以这里调整一下正则:
var matcher = /<%=([\s\S]+?)%>|<%(-[\s\S]+?)%>|<%([\s\S]+?)%>/g
来测试一下:
var text = `<% for ( var i = 0; i < list.length; i++ ) { %>
<li>
<a href="<%=list[i].url%>">
<%=list[i].name%>
</a>
</li>
<% } %>`
function template(text) {
var matcher = /<%=([\s\S]+?)%>|<%(-[\s\S]+?)%>|<%([\s\S]+?)%>/g
text.replace(matcher, function(match,interpolate, escape, evaluate, offset ) {
console.log(`match: ${match}`, `interpolate: ${interpolate}`, `escape: ${escape}`, `evaluate: ${evaluate}`, `offset: ${offset}`)
})
}
template(text)
全局匹配,成功了四次,回调函数也就执行了四次,每一次都能拿到匹配的结果,括号内部捕获的内容,捕获开始的位置索引。
但是underScore并不会做的这么简单,考虑到工具库的可扩展性,underScore支持对正则进行配置。template支持第二个参数:settings。如果用户有传递这个参数,正则也就相应的发生改变。所以这里正则不能写死。
function template(text, settings) {
var templateSettings = _.templateSettings = {
evaluate: /<%([\s\S]+?)%>/g,
interpolate: /<%=([\s\S]+?)%>/g,
escape: /<%-([\s\S]+?)%>/g
};
settings = _.extend({},templateSettings, settings)
var matcher = new RegExp([
settings.interpolate.source, // 把正则对象变成字符串
settings.escape.source,
settings.evaluate.source,
].join("|"), 'g')
console.log(matcher) // /<%=([\s\S]+?)%>|<%(-[\s\S]+?)%>|<%([\s\S]+?)%>/g
text.replace(matcher, function(match,interpolate, escape, evaluate, offset ) {
console.log(`match: ${match}`, `interpolate: ${interpolate}`, `escape: ${escape}`, `evaluate: ${evaluate}`, `offset: ${offset}`)
})
}
template('')
设置一个正则默认值templateSettings, 然后跟settings合并一下。用构造函数动态的生成正则。
这里用到了正则的source属性,转换为字符串
var evaluate = /<%([\s\S]+?)%>/g
console.log(evaluate.source) // <%([\s\S]+?)%>
第二步:获取外部
上面已经可以拿到匹配到的字符串了,接下来就是要拿到"<b><%= value %></b>"中"<b>","</b>"这些内容了。
分析上面的字符串,主要包含:<% %>,<% %>内部的,<% %>外部的,这三部分,内部的已经用正则匹配到了,剩下的就是去掉<%=' %>, <%- %>, <% %>,去掉这样的内容,和获取外部的内容:
也就是这样的字符串'<% for ( var i = 0; i < list.length; i++ ) { %>' ,转化为'<% for ( var i = 0; i < list.length; i++ ) { ',<%,%>这样的符号是需要去掉的。看下面的例子:
var reg = /<%([\s\S]+?)%>/g;
var str = '<%var%><li></li><% } %>'
var source = '', index = 0;
str.replace(reg, function(match, $1, offset) {
console.log(match, $1, offset)
source += str.slice(index, offset)
index = offset + match.length
})
console.log(source) // <li></li>
str经过这样的处理后,拿到的结果刚好是不在模版引擎内的字符串,并且去掉了<% %>字符串。
source += str.slice(index, offset)
index = offset + match.length
这样就把不需要的去掉了,把需要的也刚好都拿到了,然后把在模版引擎内部的内容和外部的内容,处理后再拼接起来,最后传递给Function。
第三步:拼接字符串
需要的内容都准备好了,接下来就是拼接成这样的js字符串了:"var _p = '<b> '+ value+' </b>'; return _p"
这里分了三种情况:
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
- <%= ... %> 插入变量, 也就是interpolate为true
这种情况下匹配到的是user[i].url。<a href="<%=list[i].url%>">,这个字符串里面的<%=list[i].url%>这一串字符串需要调整,调整为:
interpolate = 'ist[i].url' , ((__t=(" + interpolate + "))==null?'':__t) 这一串相当于 value
"'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
如果有值就直接填写这个值,没有就是空字符串。
- <%- ... %> html转义,就是escape的情况
"'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
如果有值就把这个值转码处理,防止XSS攻击,没有就是空字符串。
- <% ... %> 任意的js代码
"';\n" + evaluate + "\n__p+='";
这个最简单,任意的js代码,直接展示就可以了。
到这里可以看下字符串拼接后的样子:
__p+='';
for ( var i = 0; i < list.length; i++ ) {
__p+='
<li>
<a href="'+
((__t=(list[i].url))==null?'':__t)+
'">
'+
((__t=(list[i].name))==null?'':__t)+
'
</a>
</li>
';
}
__p+='
_p就是要一个变量,用来存贮最终的字符串,看最后面,__p+='还有一个冒号没有闭合,所以要给它一个闭合:
source += "';\n";
然后需要定义变量:
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
执行代码:
var render = new Function('list', '_', source);
var list = [ { "name": "百度", "url": "https://www.baidu.com" },
{ "name": "Daisy", "url": "https://www.baidu.com" },
{ "name": "Kelly", "url": "https://www.baidu.com" }]
console.log(render(list, _ ))
发现报错了,仔细看了看,发现拼接的字符串也没有什么问题,这里是什么原因导致的报错呢?接着往下看。
第四步:转义序列
var str = "很喜欢"设计模式"这本书"
如果以上面的这种方式来定义一个字符串,肯定会报错的,因为双引号不允许嵌套使用,但是如果需要这样展示该怎么处理呢?
- 可以换成单引号
var str = '很喜欢"设计模式"这本书'
- 使用转义
var str = "很喜欢\"设计模式\"这本书"
console.log(str) // 很喜欢"设计模式"这本书
这种由反斜杠后接字母或数字组合构成的字符组合就叫做“转义序列”。
转义序列会被视为单个字符。我们常见的转义序列还有 \n
表示换行、\t
表示制表符、\r
表示回车等等。
带有转义序列字符的字符串放在new Function()中会怎样呢?
var log = new Function("var a = '1\t23';console.log(a)");
log()
换成下面这样的:
var log = new Function("var a = '1\n23';console.log(a)");
log()
立马就报错了:
这是因为在 Function 构造函数的实现中,首先会将函数体代码字符串进行一次 ToString
操作,这时候字符串变成了:
var a = '1
23';console.log(a)
而在js中字符是不允许换行的。
这样就需要换一种写法:
var log = new Function("var a = '1\\n23';console.log(a)");
log()
正常打印结果:
underScore中总结了6中特殊字符:
var escapes = {
"'": "'", // 单引号
'\\': '\\', // 反斜杠
'\r': 'r', // 回车
'\n': 'n', // 换行
'\u2028': 'u2028', // 行分隔符
'\u2029': 'u2029' // 段落分隔符
};
以上六种都需要转换,所以需要一个正则将以上中这几种都匹配出来,然后替换:
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
然后再来执行函数:
可以正常返回。
with
再回过头来看看使用方式:
var template = _.template("<b><%- value %></b>");
var html = template({value: '<div>'});
root.innerHTML = html
这里并没有调用,Function构造函数,这个构造函数是在underScore内部调用的,而传递的也只是一个参数
template(obj),并不能像上面写的那样,直接按照名字来传递:
var render = new Function('list', '_', source);
var list = [ { "name": "百度", "url": "https://www.baidu.com" },
{ "name": "Daisy", "url": "https://www.baidu.com" },
{ "name": "Kelly", "url": "https://www.baidu.com" }]
console.log(render(list, _ ))
所以说这里接受到的不再是一个具体的名字,而是new Function('obj', '_', source); 而构造函数内部,是需要去访问:+
((__t=(list[i].url))==null?'':__t)+ ,list的,这里不能直接获取list,需要借助于with来实现:
var list = [ { "name": "百度", "url": "https://www.baidu.com" },
{ "name": "Daisy", "url": "https://www.baidu.com" },
{ "name": "Kelly", "url": "https://www.baidu.com" }]
function test(obj) {
with(obj) {
console.log(list[0].name)
}
}
test({list}) // 百度
到此为止,所有的问题都已经得到了解决,下面附上完成代码,供大家参考:
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
// NB: `oldSettings` only exists for backwards compatibility.
function template(text, settings, oldSettings) {
if (!settings && oldSettings) settings = oldSettings;
settings = defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
index = offset + match.length;
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
// Adobe VMs need the match returned to produce the correct offset.
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
var render;
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled source as a convenience for precompilation.
var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
return template;
}