mustache模板引擎

869 阅读9分钟

模板引擎是将数据变成视图方案

源码链接数据转换为视图是我们经常能够遇到的事情,但是目前来看,已经有各种各样的框架帮我们解决了这样的问题,而这篇文章主要讲了,mustache库从数据到视图到底发生了什么。

历史上将数据变为视图的方法

  1. 纯DOM法

    var arr = [
      {"name":"小明","age":12,"sex":"男"},
      {"name":"小红","age":11,"sex":"女"},
      {"name":"小强","age":13,"sex":"男"},
    ]
    
    var list = document.getElementById('list');
    
    for(var i = 0;i<arr.length;i++){
      //每遍历一项都要用DOM去创建list标签
      let oLi = document.createElement('li');
      //创建hd这个div
      let hdDiv = document.createElement('div');
      hdDiv.className = 'hd';
      hdDiv.innerText = arr[i].name + '的基本信息';
      //创建bd这个div
      let bdDiv = document.createElement('div');
      bdDiv.className = 'bd';
      let p1 = document.createElement('p');
      p1.innerText = '姓名:' + arr[i].name;
      bdDiv.appendChild(p1)
      let p2 = document.createElement('p');
      p2.innerText = '年龄:' + arr[i].age;
      bdDiv.appendChild(p2)
      let p3 = document.createElement('p');
      p3.innerText = '性别:' + arr[i].sex;
      bdDiv.appendChild(p3)
      //上树
      oLi.appendChild(hdDiv);
      //上树
      oLi.appendChild(bdDiv);
      //上树
      list.appendChild(oLi);
    
  2. 数组join法

    var arr = [
      {"name":"小明","age":12,"sex":"男"},
      {"name":"小红","age":11,"sex":"女"},
      {"name":"小强","age":13,"sex":"男"},
    ]
    var list = document.getElementById('list');
    
    //遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
    for(let i = 0;i < arr.length; i++){
      list.innerHTML += [
        '<li>',
        '  <div class="hd">' + arr[i].name + '</div>',
        '  <div class="bd">',
        '    <p>姓名:' + arr[i].name + '</p>',
        '    <p>年龄:' + arr[i].age + '</p>',
        '    <p>性别:' + arr[i].sex + '</p>',
        '  </div>',
        '</li>',
      ].join('');
    }
    
  3. ES6反引号法

    var arr = [
          {"name":"小明","age":12,"sex":"男"},
          {"name":"小红","age":11,"sex":"女"},
          {"name":"小强","age":13,"sex":"男"},
        ]
        var list = document.getElementById('list');
        //遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
        for(let i = 0;i < arr.length; i++){
          list.innerHTML += `
            <li>
              <div class="hd">${arr[i].name}</div>
              <div class="bd">
                <p>姓名:${arr[i].name}</p>
                <p>年龄:${arr[i].age}</p>
                <p>性别:${arr[i].sex}</p>
              </div>
            </li>
          `
        }
    

mustache.js

mustache是胡子的意思,因为他的嵌入标记{{}}长得像胡子,这种形式也在vue中被沿用。 在这里插入图片描述github上mustache.js自带的图片(哈哈哈哈哈哈哈哈哈哈)

使用方法简单的讲就是先定义数据,再定义模板。将数据放进一个模板里再赋值给一个变量。然后在用innerHTML写进DOM中。

官方文档案例:Mustache.render(templateStr,data)

var view = {
  title: "Joe",
  calc: function () {
    return 2 + 4;
  }
};
var output = Mustache.render("{{title}} spends {{calc}}", view);
//假设有一个id为container的div,这样我们就可以将output这个DOM元素挂载到DOM树上去了
var container = document.getElementById('container');
    container.innerHTML = output;

vue的模板语法中,我们可以使用v-for指令基于一个数组来渲染一个列表。在mustache中我们可以用{{#arr}}{{/arr}}的形式来循环data中名为arr的数组

<script>
    var templateStr = `
    <ul>
      {{#arr}}
          <li>
            <div class="hd">{{name}}基本信息</div>
            <div class="bd">
              <p>姓名:{{name}}</p>
              <p>年龄:{{age}}</p>
              <p>性别:{{sex}}</p>
            </div>
          </li>
      {{/arr}}
    </ul>
    `;
    var data = {
        arr: [
          { "name": "小明", "age": 12, "sex": "男" },
          { "name": "小红", "age": 11, "sex": "女" },
          { "name": "小强", "age": 13, "sex": "男" },
        ]
      }
    var domStr = Mustache.render(templateStr,data);
    var container = document.getElementById('container');
    container.innerHTML = domStr;

代码结果:在这里插入图片描述

有意思的是,在ES6之前没有模板字符串,有人就将上面的模板字符串放进一个不是js的script脚本中。那么这样的一个script标签中是不会被当作js执行,但是却将脚本储存了下来通过id来获取,如下

<script type="text/template" id = mytemplate>
    <ul>
      {{#arr}}
          <li>
            <div class="hd">{{name}}基本信息</div>
            <div class="bd">
              <p>姓名:{{name}}</p>
              <p>年龄:{{age}}</p>
              <p>性别:{{sex}}</p>
            </div>
          </li>
      {{/arr}}
    </ul>
</script>
<script>
    var templateStr = document.getElementById('mytemplate').innerHTML;
    var data = {
        arr: [
          { "name": "小明", "age": 12, "sex": "男" },
          { "name": "小红", "age": 11, "sex": "女" },
          { "name": "小强", "age": 13, "sex": "男" },
        ]
      }  
  </script>

同样mustache也支持我们进行数组嵌套的循环

var templateStr = `
      <ul>
        {{#arr}}
          <li>{{name}}的爱好是:
            <ol>
              {{#hobbies}}
                <li>{{.}}</li>
              {{/hobbies}}
            </ol>
          </li>
        {{/arr}}  
      </ul>
    `;
    
    var data = {
      arr:[
        {'name':'小明','age':12,'hobbies':['游泳','羽毛球']},
        {'name':'小红','age':11,'hobbies':['编程','写作文','看报纸']},
        {'name':'小强','age':13,'hobbies':['打台球']},
      ]
    };

在mustache中,还可以实现类似v-show的功能,即使布尔值是false,但是dom节点还是存在的。

var templateStr = `
      {{#m}}
        <h1>你好</h1>
      {{/m}}
    `;
    
var data = {
    m:false
};

实现mustache底层机理叭~~

我们需要做的事情很简单,就是识别双大括号,然后把数据插入,那么我们可以通过正则表达式中的replace方法,捕获双大括号内的参数,然后与data对象中对应的数值进行替换

var templateStr = '<h1>我看{{names}}天天都在{{hobby}}</h1>';
    var data = {
      name:'小红',
      hobby:'踢球'
    function render(templateStr,data){
      return templateStr.replace(/\{\{(\w+)\}\}/g,function(findStr,$1){
        return data[$1];
      })
    }
    var result = render(templateStr,data);//小红爱踢球

但是,这种正则表达式的方法难以去实现复杂的情况,比如做数组嵌套的遍历,{{.}}也无法识别之类的 所以mustache库的机理其实是将模板字符串先编译成tokens,tokens在解析的过程中结合数据生成成dom字符串

在这里插入图片描述 这里我们来讲讲Tokens,Tokens其实就是一个js的嵌套数组,抽象语法树已经虚拟节点也是从这而来,对于模板来说,标签的内容会被当作字符串来识别,那么我们就需要一个媒介来处理这些数据,让他能够识别我们的模板语法,将模板和数据分隔开。而在处理嵌套的情况,数组也可以嵌套数组来应对。

`<h1>我看{{name}}天天都在{{hobby}}</h1>`
tokens
  [
    ["text","<h1>我看"],//token
    ["name","names"],//token
    ["text","天天都在"],//token
    ["name","hobby"],//token
  ]
//复杂点的情况,有循环有嵌套
`
	<div>
	  <ol>
	    {{#students}}
	    <li>
	      学生{{name}}的爱好是
	      <ol>
	        {{#hobbies}}
	        <li>{{.}}</li>
	        {{/hobbies}}
	      </ol>
	    </li>
	    {{/students}}
	  </ol>
	</div>
`
[
  ["text", "<div><ol>"],
  ["#", "students",
    [
      ["text", "<li>学生"],
      ["name", "name"],
      ["text", "的爱好是<ol>"],
      ["#", "hobbies",
        [
          ["text", "<li>"],
          ["name", "."],
          ["text", "</li>"]
        ]
      ],
      ["text", "</ol></li>"]
    ]
  ],
  ["text", "</ol></div>"]
]

那么接下来我们将会做两件事,也是mustache底层库重点要做的两件事

1. 将模板字符串编译为tokens形式 2. 将tokens结合数据,解析为dom字符串

Scanner

  1. 首先我们要将模板字符串转换为tokens,那么我们就需要一个扫描器类Class Scanner,通过指针一个一个识别模板字符串来找到我们要的数据
  2. 然后我们可以定义一个scanUtil方法来找到{{ 或者 }},在找到的时候收集之前指针走过的数据
  3. 再定义一个scan方法,当scanUtil找到了指定的内容,让指针后移相应的长度
  4. 最后加入eos方法判断指针是否到头
class Scanner{
 constructor(templateStr) {
   this.templateStr = templateStr;
   //指针
   this.pos = 0;
   //尾巴,一开始就是模板字符串,用于检测我需要的内容是不是在第0位,因为下面我需要做判断
   this.tail =templateStr;
 }

 //走过指定内容,没有返回值
 scan(tag) {
   if(this.tail.indexOf(tag) == 0){
     this.pos += tag.length;
     //尾巴
     this.tail = this.templateStr.substring(this.pos)
   }
 }

 //让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
 scanUtil(stopTag) {
   //记录执行本方法的时候pos的值
   const pos_backup = this.pos;
   //当尾巴开头不是stopTag的时候,就说明还没有扫描到stopTag
   //&&防止找不到,找到最后也要停止
   while(!this.eos() && this.tail.indexOf(stopTag) !== 0){
     this.pos++;
     //改变尾巴为从当前指针这个字符开始,到最后的字符
     this.tail = this.templateStr.substring(this.pos)
   }

   return this.templateStr.substring(pos_backup,this.pos);
 }
 eos() {
   return this.pos >= this.templateStr.length;
 }
}

parseTemplateToTokens()

然后我们写一个函数parseTemplateToTokens()并使用上面的扫描器来把模板字符串转换为tokens。

  • 先将{{前的文字收集起来 words = scanner.scanUtil('{{');
  • 然后存入tokens中,tokens.push(['text',words]);
  • 然后用scan让指针走过大括号scanner.scan('{{');
  • 再将pos_backup到}}之间的文字收集起来words = scanner.scanUtil('}}');
  • 然后再存入tokens中,tokens.push(['name',words]);
  • 然后用scan让指针走过大括号scanner.scan('}}');
  • 在words = scanner.scanUtil('}}');中需要处理以#和/开头的字符,因为这两个字符是我们的数组循环的标记,需要单独记录下来
  • 最后再定义一个nestTokens()函数把tokens该折叠的地方折叠起来
function parseTemplateToTokens(templateStr) {
 var tokens = [];
 //创建扫描器
 var scanner = new Scanner(templateStr);
 var words;
 //扫描器工作
 while(!scanner.eos()){
   //收集开始标记出现之前的文字
   words = scanner.scanUtil('{{');
   if(words != ''){
     //去掉空格,判断普通文字的空格还是标签中的空格
     let isInJJH = false;
     //空白字符串
     var _words = '';
     for(let i = 0;i<words.length;i++){
       //判断是否在标签里
       if(words[i] == '<'){
         isInJJH = true;
       } else if(words[i] == '>'){
         isInJJH = false;
       }
       //如果不是空格,拼上
       if(words[i] != ''){
         _words += words[i];
       }else{
         //如果这项是空格,只有当它在标签内的时候,才拼接上
         if(!isInJJH) {
           _words += words[i];
         }
       }
     }
     //存text,去掉空格
     tokens.push(['text',words]);
   }    
   //过双大括号
   scanner.scan('{{');
   //收集name
   words = scanner.scanUtil('}}');
   if(words != ''){
     //这个words就是{{}}中间的东西,判断一下首字符
     if(words[0] == '#'){
       //存起来,从下标为1的项开始存,下标为0的项是#
       tokens.push(['#',words.substring(1)]);
     } else if (words[0] == '/'){
       //存起来,从下标为1的项开始存,下标为0的项是/
       tokens.push(['/',words.substring(1)]);
     } else {
       //存起来
       tokens.push(['name',words]);
     }
   }
   //过双大括号
   scanner.scan('}}');
 }

 //返回折叠收集的tokens
 return nestTokens(tokens);
}

nestTokens()

nestTokens() 在源码中作者不光使用了栈来折叠tokens,还巧妙的使用了collector收集器,让collector数组在不同的时候指向不同的数组。

  • 在开始的时候collector指向nestedTokens结果数组,让#前面token的直接push进结果数组
  • 当遇到#时候collector指向token[2],让后面的数组可以直接push进token下标为2的数组中,来实现嵌套
  • 当遇到最后一个/时候,sections会pop掉最后一个数组,sections长度为0,collector又会指向nestedTokens寻找下一个#
function nestTokens(tokens) {
  var nestedTokens = [];
  
  //栈
  var sections = [];
  
  //收集器的指向会变化,当遇见#的时候,收集器会指向这个token的下标为2的新数组
  var collector = nestedTokens;

  for(let i = 0;i<tokens.length;i++){
    let token = tokens[i];

    switch(token[0]){
      case '#':
        //收集器中放入token
        collector.push(token);
        //入栈
        sections.push(token);
        //收集器换内容,给token添加下标为2的项,并且让收集器指向它
        collector = token[2] = [];
        break;
      case '/':
        //出栈。pop会返回刚刚弹出的项
        sections.pop();
        //改变收集器为栈结构栈顶那项的下标为2的数组
        collector = sections.length > 0 ?sections[sections.length - 1][2] : nestedTokens;
        break;
      default:
        collector.push(token);
    }
  }

  return nestedTokens;
}
`
	<div>
	  <ol>
	    {{#students}}
	    <li>
	      学生{{name}}的爱好是
	      <ol>
	        {{#hobbies}}
	        <li>{{.}}</li>
	        {{/hobbies}}
	      </ol>
	    </li>
	    {{/students}}
	  </ol>
	</div>
`
[
  ["text", "<div><ol>"],
  ["#", "students",
    [
      ["text", "<li>学生"],
      ["name", "name"],
      ["text", "的爱好是<ol>"],
      ["#", "hobbies",
        [
          ["text", "<li>"],
          ["name", "."],
          ["text", "</li>"]
        ]
      ],
      ["text", "</ol></li>"]
    ]
  ],
  ["text", "</ol></div>"]
]

在这里插入图片描述这里调试了section第一次pop时候各个数组的数据辅助理解

到这里我们终于把模板字符串编译成为了我们想要的tokens,接下来我们就需要把tokens结合数据生成我们最后的dom字符串

由于我们需要结合数据,常规情况我们只需要将dom字符串在不同的情况下拼接不同的字符然后返回最后拼接成功的字符,也就是将数据与tokens中token[0]为name的那一项进行结合

//常规拼接
if(token[0]=='text'){
	resultStr += token[1]
}else if(token[0] == 'name'){
	resultStr += data[token[1]
}	

但是当data数据中存在下面情况时候,使用常规拼接的方法就无法识别了

`
{{a.b.c}}
`
var data = {
  a:{
    b:{
      c:100
    }
  }
}

那么我们可以先写一个lookup方法,功能是可以在dataObj对象中,寻找用连续点符号的keyName属性。

//拼接使用
if(token[0] == 'name'){
 //因为防止这里是a.b.c有点的形式
  resultStr += lookup(data,token[1]);
}

lookup()

当我们设别到keyName中有 . 时,那么我们可以用split方法把keyName拆分开。然后用拆分后的属性来一层一层的寻找dataObj中我们想要的data

lookup(dataObj,keyName){
  //看看keyName中有没有点符号
  if(keyName.indexOf('.') != -1 && keyName != '.'){
    //如果有点符号,那么拆开
    var keys = keyName.split('.');
    var temp = dataObj
    for (let i = 0; i < keys.length; i++) {
      temp = temp[keys[i]];
    }
    return temp;
  }
  return dataObj[keyName];
}

那么接下来我们就可以写一个renderTemplate()函数来遍历tokens,然后设变tokens数组中每一个token转换然后拼接成dom字符串啦

renderTemplate()

function renderTemplate(tokens,data){
  //结果字符串
  var resultStr = '';
  for (let i = 0; i < tokens.length; i++) {
    let token = tokens [i];
    if(token[0] == 'text'){
      resultStr += token[1];
    }else if(token[0] == 'name'){
      resultStr += lookup(data,token[1]);
    }else if(token[0] == '#'){
      resultStr += parseArray(token,data);
    }
  }
  return resultStr;
}

在这里当我们遇到token[0]='#'的时候,我们需要进行数组遍历,所以我们再写一个parseArray()函数来辅助我们识别token[0]='#'的token。

parseArray()

function parseArray(token,data){
  //得到整体数据data中这个数组要使用的部分
  var v = lookup(data,token[1]);
  //结果字符串
  var resultStr = '';
  //遍历v数组
  //遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条
  for(let i = 0;i<v.length;i++){
    //补一个"."属性
    resultStr += renderTemplate(token[2],{
      ...v[i],
      '.':v[i]
    });
  }
  return resultStr;
}

在这里调用了lookup方法来获取我们data中数组需要使用的部分,例如我们可以从data中,拿到arr这个部分

{{#arr}}
  <li>{{name}}的爱好是:
      <ol>
        {{#hobbies}}
          <li>{{.}}</li>
        {{/hobbies}}
      </ol>
  </li>
{{/arr}}

var data = {
 arr: [
    { name: '小明', age: 12, hobbies: ['游泳', '羽毛球'] },
    { name: '小红', age: 11, hobbies: ['编程', '写作文', '看报纸'] },
    { name: '小强', age: 13, hobbies: ['打台球'] }
  ]
}
//得到
	{ name: '小明', age: 12, hobbies: ['游泳', '羽毛球'] },
    { name: '小红', age: 11, hobbies: ['编程', '写作文', '看报纸'] },
    { name: '小强', age: 13, hobbies: ['打台球'] }

之后我们再遍历的过程中结合renderTemlate()函数来做递归调用的事情。在pasreArray中调用renderTemplate时,第二个参数需要考虑到tokens是简单模板(用 **.**来循环 )还是复杂模板用对象来循环。所以我们为第二个参数进行了简单的处理,把找到的数据展开,并添加一个 . 属性。这样我们的数据就可以支持两种情况。

最后我们只需要写一个render函数parseTemplateToTokens()和renderTemplate()结合起来,我们的盗版musatche就完成了

render(templateStr,data){
    //模板字符串能够变为tokens数组
    var tokens = parseTemplateToTokens(templateStr);
    //tokens数组变成dom字符串
    var domStr = renderTemplate(tokens,data)
    return domStr;
  }

已经将此方法添加到了全局的Sxi_templateEngine方法上

使用山寨mustache处理数据

<!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>
<body>
  <div id="container"></div>
  <script src="/xuni/bundle.js"></script>

  <script>
    var templateStr = `
      <div>
        <ul>
          {{#students}}
            <li>{{name}}的爱好是:
              <ol>
                {{#hobbies}}
                  <li>{{.}}</li>
                {{/hobbies}}
              </ol>
            </li>
          {{/students}}  
        </ul>
      <div/>
    `;
    var data = {
      students:[
        {'name':'小明','hobbies':['游泳','健身']},
        {'name':'小红','hobbies':['足球','篮球','羽毛球']},
        {'name':'小强','hobbies':['吃饭','睡觉']},
      ]
    };
    //调用render
    var domStr = Sxi_TemplateEngine.render(templateStr,data);
    //渲染上树
    var container = document.getElementById('container');
    container.innerHTML = domStr;
  </script>
</body>
</html>

运行结果在这里插入图片描述

参考:B站邵山欢