最简单的JavaScript模板引擎

什么是JavaScript引擎

其实在网站开发中模板还是很常见的一种技术,比如PHP的Smarty、ASP.NET的Master Page等,但这些模板都是基于服务器的,JavaScript模板引擎是为了解决我们在前端写出形如这样的拼html的语句

var html='<ul>';
for(var i=0;i<users.length;i++){
  html+='<li><a href=">'+users[i].url+'">'+users[i].name+'</a>';
}
html+='</ul>';
document.getElementById('results').innerHTML=html;

上面的代码我们一看就知道是在拼html,但具体拼的什么很难说清,需要逐句去读代码,如果我们有这样一个模板

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

看了很容易就明白开发者希望得到的是这样的 html

<ul>
  <li><a href="XXX">OOO</a></li>
  <li><a href="XXX">OOO</a></li>
  <li><a href="XXX">OOO</a></li>
</ul>

JavaScript 模板引擎就是帮我们把带有 JavaScript 代码的伪 html 语句翻译为 html 的东东

John Resig 的实现方式

先看看 John Resig 是怎么实现最简单的一个 JavaScript 模板引擎的

// Simple JavaScript Templating
 // John Resig - http://ejohn.org/ - MIT Licensed
 (function(){
   var cache = {};
   this.tmpl = function tmpl(str, data){
     // Figure out if we're getting a template, or if we need to
     // load the template - and be sure to cache the result.
     var fn = !/\W/.test(str) ?
       cache[str] = cache[str] ||
         tmpl(document.getElementById(str).innerHTML) :
       // Generate a reusable function that will serve as a template
       // generator (and which will be cached).
       new Function("obj",
         "var p=[],print=function(){p.push.apply(p,arguments);};" +
         // Introduce the data as local variables using with(){}
         "with(obj){p.push('" +
         // Convert the template into pure JavaScript
         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('');");
     // Provide some basic currying to the user
     return data ? fn( data ) : fn;
   };
 })();

看完上面代码就明白的同学就不用看下面内容了,没太明白的同学可以和我一块儿看看着三十多句代码为什么能够实现一个 JavaScript 引擎吧。

模板的语法

模板的语法很简单,有三条基本规则

  1. 用正常的方式书写 html
  2. 用 <% %> 嵌套 JavaScript 语句
  3. 用 <%= %> 嵌套 JavaScript 变量值

我们的 JavaScript 引擎正式设计为识别这种类型的模板的,拿上面的做例子,这样的一个模版

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

想得到预期 html 字符串,我们必须设法让模板内部的 javascript 变量置换、javaScript 语句执行,也就是把 JavaScript 代码剥离出来执行,把其它 html 语句拼接为一个字符串

var p=[];

p.push('<ul>');
for(var i=0;i<users.length;i++){ //javascript语句执行
  p.push('<li><a href="'); //html语句拼接
  p.push(users[i].url); //javascript变量置换后拼接
  p.push('">');
  p.push(users[i].name);
  p.push('</a></li>');
}
p.push('</ul>');

最后得到的数组 join 一下就是我们希望得到的字符串了,首先需要取到模板内的字符串,这个简单按照 John 的做法我们可以把模板放到一个 script 标签里(防止在页面显示出来),换成我们特定的类型

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

这样就可以通过 document.getElementById(str).innerHTML 来获取模版内字符串了,然后我们应用一些简单的法则处理一下模板内字符串

<%=xxx%>           =>     ');p.push(xxx);p.push('

<%                 =>     ');

%>                 =>     p.push('

这样我们就可以得到这样的结构,看起来就已经很接近结果了

p.push('<ul>');
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('</ul>');

现在我们根据上面规则做替换了,这里得使用一些正则表达式和 replace 函数的知识,不太熟悉的同学可能需要看看 JavaScript 正则表达式上——基本语法 JavaScript 正则表达式下——相关方法

  1. <%=xxx%> 替换为 ');p.push(xxx);p.push('
html=html.replace(/<%=(.*?)%>/g,"');p.push(xxx);p.push('");
  1. <% 替换为 ');
html=html.replace(/<%/g,"');");
  1. %> 替换为 p.push('
html=html.replace(/%>/g,"p.push('");

我们再把结果用 p.push(‘和’); 包裹起来就可以看到初步效果了

function tmpl(id,data){
        var html=document.getElementById(id).innerHTML;
        var result="var p=[]; p.push('"
            +html.replace(/<%=(.*?)%>/g,"');p.push(xxx);p.push('")
            .replace(/<%/g,"');")
            .replace(/%>/g,"p.push('")
            +" ');";
    }

这样我们就把 html 模版内容替换成了这样的一个字符串

var result="
var p=[];
p.push('<ul>');
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('</ul>');"

貌似得到结果了,但我们得到的是字符串,我们预期的是这个字符串执行的结果,很多同学会想到使用 eval 就可以让字符串变成 JavaScript 语句执行,但是 Jonh 使用了另外一种方式——创建 function,我们知道除了常用使用 function 关键字创建一个 function

function fn(data){
    console.log(data);
}

还可以使用 Function 构造函数来创建一个 function

var fn = new Function(arg1, arg2, ..., argN, function_body)

在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码),使用这种方式可以动态(方法体是动态生成的,提前不知道,当然这样做会有效率问题)创建一个方法,也就是说我们还可以使用刚才拼出来的 javascript 字符串动态创建一个函数

function tmpl(id,data){
        var html=document.getElementById(id).innerHTML;
        var result="var p=[];p.push('"
            +html.replace(/[\r\n\t]/g," ")
            .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('")
            .replace(/<%/g,"');")
            .replace(/%>/g,"p.push('")
            +"');return p.join('');";
        var fn=new Function(data,result);
        return fn(data);
    }

这样看起来很科学了,但是我们执行一下会报错,原因很简单就是参数的作用域不对,我们需要改变一下动态构造的方法的作用域,这个有很多方式比如 apply 函数啊什么的,我们暂且采用 John 的方式——使用 with 关键字改变作用域

function tmpl(id,data){
        var html=document.getElementById(id).innerHTML;
        var result="var p=[];with(obj){p.push('"
            +html.replace(/[\r\n\t]/g," ")
            .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('")
            .replace(/<%/g,"');")
            .replace(/%>/g,"p.push('")
            +"');}return p.join('');";
        var fn=new Function("obj",result);
        return fn(data);
    }

虽然看起来和 John 的方法还有很大区别,不过我们已经偷师到了其精髓,实现了一个最简单 JavaScript 模版引擎,你是不是也明白了 JavaScript 模版引擎是什么了呢?就是简单的字符串替换,剥离出 JavaScript 语句,然后利用新的字符串构造函数,返回结果。

看个例子

<!DOCTYPE html>
<html>
<head>
    <title>Template</title>
</head>
<body>
    <div></div>
    <script type="text/html">
        <ul>
            <% for ( var i = 0; i < users.length; i++ ) { %>
            <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
            <% } %>
        </ul>
    </script>
    <script type="text/javascript">
    var results = document.getElementById("results");
    var users=[
        {"name":"Byron", "url":"http://localhost"},
        {"name":"Casper", "url":"http://localhost"},
        {"name":"Frank", "url":"http://localhost"}
    ];

    function tmpl(id,data){
        var html=document.getElementById(id).innerHTML;
        var result="var p=[];with(obj){p.push('"
            +html.replace(/[\r\n\t]/g," ")
            .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('")
            .replace(/<%/g,"');")
            .replace(/%>/g,"p.push('")
            +"');}return p.join('');";
        var fn=new Function("obj",result);
        return fn(data);
    }

    results.innerHTML = tmpl("user_tmpl", users);
</script>
</body>
</html>

原文地址 链接