underScore之模板引擎(上)

原文来自

前言

underscore提供了模板引擎的功能

1
2
3
4
var tpl = 'hello : <%= name %>';

var compiled = _.template(tpl);
compiled({name: 'blue'}); // "hello: blue"

感觉好像没有什么强大的地方,再举个例子

在 HTML 文件中:

1
2
3
4
5
6
7
8
9
10
11
<ul id="name_list"></ul>

<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>

javaScript 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var container = document.getElementById('name_list');

var data = {
users: [
{ "name": "Kevin", "url": "http://localhost" },
{ "name": "Daisy", "url": "http://localhost" },
{ "name": "Kelly", "url": "http://localhost" }
]
}

var precompile = _.template(document.getElementById('user_tmpl').innerHTML);
var html = precompile(data);

container.innerHTML = html;

实现思路

underscore的template函数参考了jQuery的作者john Resig 在2008年发表的一篇文章 javaScript Micro-Templating

依然是以这段模板字符串为例:

1
2
3
4
5
6
7
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>

john Resig 的思路是将这段代码转换成这样一段程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 模拟数据
var users = [{
"name" : 'blue',
"url" : 'http://localhost',
}];

var p = [];
for(var i = 0; i < users.length; i++) {
p.push('<li><a href="');
p.push(users[i].url);
p.push('">');
p.push(users[i].name);
p.push('</a></li>');
}

// 最后 join 一下就可以得到最终拼接好的模板字符串
console.log(p.join(''))

我们注意到,模板其实就是一段字符串,我们怎么根据一段字符串生成一段代码呢?很容易就想到用eval,那就是用eval吧,

然后我们会发现,为了转换成这样一段代码,我们需要将<%xxx%>转换为 xxx ,其实也就是去掉包裹的符号,还要将 <%=xxx%> 转化成 p.push(xxx),这些都可以用正则实现,但是我们还需要写 p.push(‘

  • ‘); 这些又该如何实现呢?
  • 那我们换个思路,依然是用正则,但是我们

    1. 将 %> 替换成 p.push(‘
    2. 将 <% 替换成 ‘);
    3. 将 <%=xxx%> 替换成 ‘);p.push(xxx);p.push(‘

    来举个例子

    1
    2
    3
    4
    5
    6
    7
    <%for ( var i = 0; i < users.length; i++ ) { %>
    <li>
    <a href="<%=users[i].url%>">
    <%=users[i].name%>
    </a>
    </li>
    <% } %>

    照着这个规则,上面的代码会被替换成

    1
    2
    3
    4
    5
    6
    7
      ');for ( var i = 0; i < users.length; i++ ) { p.push('
    <li>
    <a href="');p.push(users[i].url);p.push('">
    ');p.push(users[i].name);p.push('
    </a>
    </li>
    '); } p.push('

    这样肯定会报错,毕竟代码都没有写全,我们在首和尾加上部分代码,变成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 添加的首部代码
    var p = []; p.push('

    ');for ( var i = 0; i < users.length; i++ ) { p.push('
    <li>
    <a href="');p.push(users[i].url);p.push('">
    ');p.push(users[i].name);p.push('
    </a>
    </li>
    '); } p.push('

    // 添加的尾部代码
    ');

    整理下这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var p = []; p.push('');
    for(var i = 0; i < users.length; i++) {
    p.push('<li><a href="');
    p.push(users[i].url);
    p.push('">');
    p.push(users[i].name);
    p.push('</a></li>');
    }

    p.push('');

    恰好可以实现这个功能,不过还要注意一点,要将换行符替换成空格,防止解析成代码的时候报错,不过为了这里能够方便理解原理,就只在代码中实现。

    第一版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        // 第一版
    function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var string = "var p = []; p.push('" +
    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');"

    eval(string)

    return p.join('');
    };

    为了验证是否有用:
    HTML文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <script type="text/html" id="user_tmpl">
    <%for ( var i = 0; i < users.length; i++ ) { %>
    <li>
    <a href="<%=users[i].url%>">
    <%=users[i].name%>
    </a>
    </li>
    <% } %>
    </script>

    javaScript文件

    1
    2
    3
    4
    5
    6
    var users = [
    { "name": "Byron", "url": "http://localhost" },
    { "name": "Casper", "url": "http://localhost" },
    { "name": "Frank", "url": "http://localhost" }
    ]
    tmpl("user_tmpl", users)

    Function

    在这里我们使用了 eval, 实际上 john Resig 在文章中使用的是Function构造函数。
    Function 构造函数创建一个新的Function对象,在JavaScript中,每个函数实际上都是一个Function对象。
    使用方法为:

    1
    new Function([arg1[, arg2[, ...argN]], ] functionBody)

    arg1, arg2, … argN 表示函数用到的参数,functionBody表示一个含有包括函数定义的JavaScript语句的字符串。

    1
    2
    3
    var adder = new Function("a", "b", "return a + b");

    adder(2, 4); //6

    第二版

    使用Function构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
      // 第二版
    function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var fn = new Function("obj",

    "var p = []; p.push('" +

    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');return p.join('');");

    return fn(data);
    };

    使用方法依然跟第一版相同

    不过值得注意的是:其实tmpl函数没有必要传入data参数,也没有必要在最后return的时候,传入data参数,即使你把这两个参数都去掉,代码还是可以正常执行的。

    使用Function构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,不能访问Function构造器被调用生成的上下文的作用域。这和使用带有函数表达式代码的eval不同。这也就是为什么去掉参数后,它依然能正常执行,本地变量没有users,便使用到了全局变量users,从而获取到了数据

    with

    现在有一个小问题,实际上我们传入的数据结构可能比较复杂,如:

    1
    2
    3
    4
    5
    var data = {
    status: 200,
    name: 'blue',
    frieds: [...]
    }

    如果我们将这个数据结构传入tmpl函数中,在模板字符串中,如果要用到某个数据,总是需要使用data.namedata.friends、的形式来获取,麻烦就麻烦在我想直接使用name、friends等变量,而不是繁琐的使用 data. 来获取。

    这又该如何实现呢?答案是with

    with语句可以扩展一个语句的作用域链(scope chain)。当要多次访问一个对象的时候,可以使用with做简化。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    var hostName = location.hostname;
    var url = location.href;

    //使用with
    with(location){
    var hostName = hostname;
    var url = href;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person() {
    this.name = 'blue';
    this.age = '18';
    }
    var person = new Person();

    with(person) {
    console.log('my name is ' + name + ', age is ' + age + '.')
    }
    // my name is blue, age is 18.

    然而并不推荐使用with语句, 因为他可能是混淆错误和兼容性问题的根源,除此之外,也会造成性能低下

    第三版

    使用了 with

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      // 第三版
    function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var fn = new Function("obj",

    // 其实就是这里多添加了一句 with(obj){...}
    "var p = []; with(obj){p.push('" +

    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');}return p.join('');");

    return fn(data);
    };

    第四版

    如果我们的模板不变,数据却发生了变化,如果使用我们之前写的tmpl函数,每次都会new Function,这其实是没有必要的,如果我们能在使用tmpl的时候,返回一个函数,然后使用该函数,传入不同的数据,只根据数据不同渲染不同的html字符串,就可以避免这种无所谓的损失。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 第四版
    function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var fn = new Function("obj",

    "var p = []; with(obj){p.push('" +

    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');}return p.join('');");

    var template = function(data) {
    return fn.call(this, data)
    }
    return template;
    };

    // 使用时
    var compiled = tmpl("user_tmpl");
    results.innerHTML = compiled(data);