实现模板引擎

概念


模板引擎(这里指的时用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档

模板引擎不属于特定技术领域,它是跨领域平台的概念。在Asp下有模板引擎,在PHP也有模板引擎,在#C下也有,甚至JavaScriptWinForm开发会用到的模板引擎技术。

举例:在一个ul里插入lili包含nameage,最后生成结构如下

  • XXX - 20

实现方式:
1.拼接HTML 字符串

var html = '
  • ' + name + ' - ' + age + '
  • '; var ulEl = document.getElementsByTagName('ul')[0]; ulEl.innerHTML = html;

    2.构造DOM

    var ulEl = document.getElementsByTagName('ul')[0];
    var liEl = document.createElement('li');
    liEl.textContent= name + ' - ' + age;
    ulEl.appendChild(liEl);
    

    但这种方式写起来比较麻烦,下面就在方式1的基础上进行优化

    字符串格式化


    如果想要字符串不包含变量,跟变量区分开,而是一个完整的字符串,不含加号,方便书写。这时就需要用特定的格式代替变量,格式化前后变化如下:

    // before
    var li = '
  • ' + obj[i].name + '-' + obj[i].age + '
  • '; // after var li = stringFormat('
  • {0}-{1}
  • ', obj[i].name, obj[i].age);

    将字符串格式化需封装stringFormat,变量用{}括起来表示,传入参数的按顺序放入{0}{1}上,由函数返回字符串
    声明函数时,如果需传入多个参数就需要写多个形参,然而一个函数最多可以有255个参数,参数太多书写时影响美观,直接传入需要处理的字符串即可

    function stringFormat(str){
      // arguments 是传入参数的类数组,不可直接使用数组的方法,不能直接写 arguments.slice(1)
      var params = [].slice.call(arguments, 1);
      // 用正则表达式将 {} 替换成对应变量的值
      var reg = /\{(\d+)\}/g;
      str= str.replace(reg, function(){
        console.log(arguments);
        var index = arguments[1];
        return params[index];
      });
      
      return str;
    }
    
    console.log(stringFormat('{0} - {1}', 'xxx', 20));
    

    运行结果如下


    字符串格式化之后效率提升不少,但是还有个问题,如果传入的参数没按顺序,完全打乱,那出来的结果也不是预期中的,还需要进一步改进,接下来就要说到本文的重点了——模板引擎

    模板引擎


    模板引擎其实就是字符串格式化的升级版
    在上述的例子基础上进行改进,可以将数据存到一个对象里,可将{0}{1}分别替换成{name}{age},也就是对象里的属性。代码如下:

    function stringFormat(str, data){
      var params = [].slice.call(arguments, 1);
      var reg = /\{(\S+)\}/g;
      str= str.replace(reg, function(){
        console.log(arguments);
        var index = arguments[1];
        return data[index];
      });
      
      return str;
    }
    
    var data = {
        name: "Krasimir",
        age: 29
    };
    
    console.log(stringFormat('{name} - {age}', data));
    

    运行结果如下:


    还有另一种方法,为跟上一个例子做点区分,将模板换成<%property%>,于是可将{name}{age}分别替换成<%name%><%age%>,用到了reg.exec遍历查找传入字符串,匹配模板,然后用str.replace替换成对应的属性值

    先了解exec的作用,代码和运行结果如下:


    从结果可看出:

    • reg不是全局匹配,则每次运行reg.exec(str)得到的结果都是字符串中第一个匹配的数组
    • reg是全局匹配,则每次运行reg.exec(str)得到的结果都按顺序会往下匹配,当全部匹配完之后,再执行一次reg.exec(str)则返回null

    利用reg.exec匹配之后再替换,代码如下:

    var TemplateEngine = function(tpl, data){
      var reg = /<%([^%>]+)?%>/g;
      var match;
      while(match = reg.exec(tpl)){
        console.log(match);
        tpl = tpl.replace(match[0], data[match[1]]);
      }
      return tpl;
    };
    
    var template = '

    Hello, my name is <%name%>. I\'m <%age%> years old.

    '; var data = { name: "Krasimir", age: 29 }; var string = TemplateEngine(template, data); console.log(string);

    运行结果如下:


    一个简易版的模板引擎就实现了,但如果数据是这样的,如下所示:

    var data = {
        name: "Krasimir",
        info: {
          age: 29
        }
    };
    

    要取出age的值,传入的模板字符串改成var template = '

    Hello, my name is <%name%>. I\'m <%info.age%> years old.

    'TemplateEngine里匹配到并返回的就是data["info.age"],这样得不到正确的age属性值
    再者,如果想插入多行字符串,上面这方法就得执行多次;如果想在字符串中插入JavaScript代码,循环插入多行字符串,实现如下代码:

    var template = 
    'My skills:' + 
    '<%if(this.showSkills) {%>' +
        '<%for(var index in this.skills) {%>' + 
        '<%this.skills[index]%>' +
        '<%}%>' +
    '<%} else {%>' +
        '

    none

    ' + '<%}%>'; var string = TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true }) document.body.innerHTML = string

    也就是说TemplateEngine函数输出的结果得是:

    var line = "";
    line += "My skills:";
    if(this.showSkills) {
      for(var index in this.skills) {
        line += "";
        line += this.skills[index];
        line += "";
      }
    } else {
      line += "

    none

    "; } return line;

    返回的结果得当作JavaScript代码来执行
    怎么把字符串当作代码来执行呢?Function 构造函数了解一下

    new Functon (arg1, arg2, ... argN, functionBody)
    每个arg都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串
    举例:

    function sayHi(sName, sMessage) {
      alert("Hello " + sName + sMessage);
    }
    

    还可以定义成:

    var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);");
    

    函数主体字符串若涉及到双引号则需进行转义(\"

    有了可以将字符串当成函数主体执行的函数构造器,于是TemplateEngine的封装如下:

    var TemplateEngine = function(tpl, data){
      var reg = /<%([^%>]+)?%>/g;
      var regJS = /^( )?(^if|else|switch|case|default|break|for|{|})(.*)/g;
      var cursor = 0;
      var code = 'var line = "";\n';
      var match;
      
      var isExp = function(str){
        if(str.match(regJS)){
          return str;
        }else {
          return 'line += ' + str + ';';
        }
      };
      
      while(match = reg.exec(tpl)){
        if(cursor !== match.index){
          code += 'line += "' + tpl.slice(cursor, match.index).replace(/"/g, '\\"') + '";\n';
        }    
        code += isExp(match[1])+'\n';
        cursor = match.index + match[0].length;
      }
      code += tpl.substr(cursor, tpl.length - 1);
      code += 'return line;';
      console.log(code);
      var str = new Function(code.replace(/[\r\t\n]/g, '')).apply(data);
      return str;
    };
    
    var template = 
    'My skills:' + 
    '<%if(this.showSkills) {%>' +
        '<%for(var index in this.skills) {%>' + 
        '<%this.skills[index]%>' +
        '<%}%>' +
    '<%} else {%>' +
        '

    none

    ' + '<%}%>'; var string = TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true }); document.body.innerHTML = string;

    控制台打印如下:


    • showSkills: true
      文档中插入html效果:

      showSkills: true

      再打开调式看下生成的html,如下所示:
      showSkills: true

    • showSkills: false
      为了验证准确性,再将showSkills改为false
      文档中插入html效果:

      showSkills: false

      再打开调式看下生成的html,如下所示:
      showSkills: false

    更具体的思路可参考只有20行Javascript代码!手把手教你写一个页面模板引擎

    原理


    介绍了以上几种实现模板引擎的方法之后,可知其实现原理如下:


    将模板和数据输入到模板引擎,执行该函数,输出结果即HTML代码段,再将该片段插入到文档中
    模板引擎可用于任意一端,前后端即插即用,不局限于生成内容的语法,只要生成内容为字符串文本即可。然而,此模板引擎依赖于innerHTML,存在脚本注入的风险;且对于数据的更改,需要重新渲染模板,所以在初次渲染和之后的模板更新需要耗费同样的资源

    参考


    • 实现一个简单的模板引擎
    • Juicer – 一个 JavaScript 模板引擎的实现和优化
    • https://blog.csdn.net/kjfcpua/article/details/7295451
    • ECMAScript Function 对象(类)

    你可能感兴趣的:(实现模板引擎)