zoukankan      html  css  js  c++  java
  • JavaScript 模板引擎实现原理解析

    1、入门实例

    首先我们来看一个简单模板:

      <script type="template" id="template">
        <h2>
          <a href="{{href}}">
            {{title}}
          </a>
        </h2>
        <img src="{{imgSrc}}" alt="{{title}}">
      </script>

    其中被{{ xxx }}包含的就是我们要替换的变量。
    接着我们可能通过ajax或者其他方法获得数据。这里我们自己定义了数据,具体如下:

    var data = [
        {
          title: "Create a Sticky Note Effect in 5 Easy Steps with CSS3 and HTML5",
          href: "http://net.tutsplus.com/tutorials/html-css-techniques/create-a-sticky-note-effect-in-5-easy-steps-with-css3-and-html5/",
          imgSrc: "https://d2o0t5hpnwv4c1.cloudfront.net/771_sticky/sticky_notes.jpg"
        },
        {
          title: "Nettuts+ Quiz #8",
          href: "http://net.tutsplus.com/articles/quizzes/nettuts-quiz-8-abbreviations-darth-sidious-edition/",
          imgSrc: "https://d2o0t5hpnwv4c1.cloudfront.net/989_quiz2jquerybasics/quiz.jpg"
        }
      ];

    ok,现在的问题就是我们怎么把数据导入到模板里面呢?

    第一种大家会想到的就是采用replace直接替换里面的变量:

    template = document.querySelector('#template').innerHTML,
    result = document.querySelector('.result'),
    i = 0, len = data.length,
    fragment = '';
     
    for ( ; i < len; i++ ) {
        fragment += template
          .replace( /{{title}}/, data[i].title )
          .replace( /{{href}}/, data[i].href )
          .replace( /{{imgSrc}}/, data[i].imgSrc );
    }
     
    result.innerHTML = fragment;

    第二种的话,相对第一种比较灵活,采用的是正则替换,对于初级前端,很多人对正则掌握的并不是很好,一般也用的比较少。具体实现如下:

    template = document.querySelector('#template').innerHTML,
    result = document.querySelector('.result'),
    attachTemplateToData;
     
    // 将模板和数据作为参数,通过数据里所有的项将值替换到模板的标签上(注意不是遍历模板标签,因为标签可能不在数据里存在)。
    attachTemplateToData = function(template, data) {
            var i = 0,
                len = data.length,
                fragment = '';
     
            // 遍历数据集合里的每一个项,做相应的替换
            function replace(obj) {
                var t, key, reg;
           
           //遍历该数据项下所有的属性,将该属性作为key值来查找标签,然后替换
                for (key in obj) {
                    reg = new RegExp('{{' + key + '}}', 'ig');
                    t = (t || template).replace(reg, obj[key]);
                }
     
                return t;
            }
     
            for (; i < len; i++) {
                fragment += replace(data[i]);
            }
     
            return fragment;
        };
     
    result.innerHTML = attachTemplateToData(template, data);

     与第一种相比较,第二种代码看上去多了,但是功能实则更为强大了。第一种我们需要每次重新编写变量名,如果变量名比较多的话,会比较麻烦,且容易出错。第二种的就没有这些烦恼。

    2、模板引擎相关知识

    通过上面的例子,大家对模板引擎应该有个初步的认识了,下面我们来讲解一些相关知识。

    2.1 模板存放

    模板一般都是放置到 textarea/input 等表单控件,或者 script 等标签中。比如上面的例子,我们就是放在 script 标签上的。

    2.2 模板获取

    一般都是通过ID来获取,document.getElementById(“ID”):

    //textarea或input则取value,其它情况取innerHTML
    var html = /^(textarea|input)$/i.test(element.nodeName) ? element.value : element.innerHTML;

    上面的是通用的模板获取方法,这样不管你是放在 textarea/input 还是 script 标签下都可以获取到。

    2.3 模板函数

    一般都是templateFun("id", data);其中id为存放模板字符串的元素id,data为需要装载的数据。

    2.4 模板解析编译

    模板解析主要是指将模板中 JavaScript 语句和 html 分离出来,编译的话将模板字符串编译成最终的模板。上面的例子比较简单,还没有涉及到模板引擎的核心。

    2.5 模板分隔符

    要指出的是,不同的模板引擎所用的分隔符可能是不一样,上面的例子用的是{{ }},而Jquery tmpl 使用的是<%  %>。

    3、jQuery tmpl 实现原理解析

     jQuery tmpl是由jQuery的作者写的,代码短小精悍。总共20多行,功能却比我们上面的强大很多。我们先来看一看源码:

    (function(){
      var cache = {};
     
      this.tmpl = function tmpl(str, data){
       
        var fn = !/W/.test(str) ? 
          cache[str] = cache[str] ||
            tmpl(document.getElementById(str).innerHTML) :
        
          new Function("obj",
            "var p=[],print=function(){p.push.apply(p,arguments);};" +
           
            "with(obj){p.push('" +
           
            str
              .replace(/[
    	
    ]/g, " ") 
              .split("<%").join("	") 
              .replace(/((^|%>)[^	]*)'/g, "$1
    ")
              .replace(/	=(.*?)%>/g, "',$1,'")  
              .split("	").join("');")  
              .split("%>").join("p.push('") 
              .split("
    ").join("\'")
          + "');}return p.join('');");
       
        return data ? fn( data ) : fn;
      };
    })();

    初看是不是觉得有点懵,完全不能理解的代码。没事,后面我们会对源码进行解释的,我们还是先看一下所用的模板

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

    可以发现,这个模板比入门例子的模板更为复杂,因为里面还夹杂着 JavaScript 代码。JavaScript 代码采用 <%  %> 包含。而要替换的变量则是用 <%=   %> 分隔开的。

    下面我再来对代码做个注释。不过即使看了注释,你也不一定能很快理解,最好的办法是自己实际动手操作一遍。

    // 代码整个放在一个立即执行函数里面
    (function(){ // 用来缓存,有时候一个模板要用多次,这时候,我们直接用缓存就会很方便
    var cache = {};
    // tmpl绑定在this上,这里的this值得是window
    this.tmpl = function tmpl(str, data){
    // 只有模板才有非字母数字字符,用来判断传入的是模板id还是模板字符串,
    // 如果是id的话,判断是否有缓存,没有缓存的话调用tmpl;
    // 如果是模板的话,就调用new Function()解析编译
    var fn = !/W/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : new Function("obj",
         // 注意这里整个是字符串,通过 + 号拼接
    "var p=[],print=function(){p.push.apply(p,arguments);};" + "with(obj){p.push('" + str
          // 去除换行制表符 .replace(
    /[ ]/g, " ")
          
          // 将左分隔符变成
    .split(
    "<%").join(" ")
          
          // 去掉模板中单引号的干扰 .replace(
    /((^|%>)[^ ]*)'/g, "$1 ")
          
          // 为 html 中的变量变成 ",xxx," 的形式, 如: =users[i].url%> 变成 'users[i].url,'
          // 注意这里只有一个单引号,还不配对
    .replace(
    / =(.*?)%>/g, "',$1,'")
          
          // 这时候,只有JavaScript 语句前面才有 " ", 将 变成 ');
          //
    这样就可把 html 标签添加到数组p中,而javascript 语句 不需要 push 到里面。

          .split(
    " ").join("');")
          
          // 这时候,只有JavaScript 语句后面才有 "%>", 将 %> 变成 p.push('
          // 上一步我们再 html 标签后加了 ');, 所以要把 p.push(' 语句放在 html 标签放在前面,这样就可以变成 JavaScript 语句
    .split(
    "%>").join("p.push('")
          // 将上面可能出现的干扰的单引号进行转义
          .split(
    " ").join("\'")
        // 将数组 p 变成字符串。
    + "');}return p.join('');"); return data ? fn( data ) : fn; }; })();

    上面代码中,有一个要指出的就是new Function 的使用 方法。给 new Function() 传一个字符串作为函数的body来构造一个 JavaScript函数。编程中并不经常用到,但有时候应该是很有用的。

    下面是 new Function 的基本用法:

    // 最后一个参数是函数的 body(函数体),类型为 string; 
    // 前面的参数都是 索要构造的函数的参数(名字) 
    var myFunction = new Function('users', 'salary', 'return users * salary'); 

    最后的字符串就是下面这种形式:

      var p = [],
        print = function() {
          p.push.apply(p, arguments);
        };
      with(obj) {
        p.push('     <ul>     ');
        for (var i = 0; i < users.length; i++) {
          p.push('          <li><a href="', users[i].url, '">', users[i].name, '</a></li>     ');
        }
        p.push('   </ul> ');
      }
      return p.join('');

    里面的 print 函数 在我们的模板里面是没有用到的。

    要指出的是,采用 push 的方法在 IE6-8 的浏览器下会比 += 的形式快,但是在现在的浏览器里面, += 是拼接字符串最快的方法。实测表明现代浏览器使用 += 会比数组 push 方法快,而在 v8 引擎中,使用 += 方式比数组拼接快 4.7 倍。所以 目前有些更高级的模板引擎会 根据 javascript 引擎特性采用了两种不同的字符串拼接方式。

    下面的代码是摘自腾讯的 artTemplate 的, 根据浏览器的类型来选择不同的拼接方式。功能越强大,所考虑的问题也会更多。

        var isNewEngine = ''.trim;// '__proto__' in {}
        var replaces = isNewEngine
        ? ["$out='';", "$out+=", ";", "$out"]
        : ["$out=[];", "$out.push(", ");", "$out.join('')"];

    挑战:有兴趣的可以改用 += 来实现上面的代码。


    总结

    模板引擎原理总结起来就是:先获取html中对应的id下得innerHTML,利用开始标签和关闭标签进行字符串切分,其实是将模板划分成两部份内容,一部分是html部分,一部分是逻辑部分,通过区别一些特殊符号比如each、if等来将字符串拼接成函数式的字符串,将两部分各自经过处理后,再次拼接到一起,最后将拼接好的字符串采用new Function()的方式转化成所需要的函数。

    目前模板引擎的种类繁多,功能也越来越强大,不同模板间实现原理大同小异,各有优缺,请按需选择。

    参考文章:

    2、JavaScript模板引擎的应用场景及实现原理

    3、大叔手记(7):构建自己的JavaScript模板小引擎

  • 相关阅读:
    scala的Class
    scala的Map
    log4j配置文件详细解释
    学习线程1之建立线程,并启动
    Spring中WebApplicationContext的研究
    Log4j 配置 的webAppRootKey参数问题
    JNDI绑定数据库
    Struts2配置之Struts.properties
    Java多线程-工具篇-BlockingQueue
    StringUtils判断字符串是否为空的方法
  • 原文地址:https://www.cnblogs.com/huansky/p/6073104.html
Copyright © 2011-2022 走看看