underScore专题-template模版引擎

从使用方法分析

先来看一个例子,了解一下什么是模版引擎:

    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;
  }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值