Mustache

mustache: 中文意思是:髭;上唇的胡子;长髭

它是一款经典的前端模板引擎,在前后端分离的技术架构下面,一度流行。之前也用过 art-template 之类的模板插件,应该也是同样的原理。如今随着 前端三大框架 的流行,这种方式已经深入前端人心。但是我还是第一次听到这个框架,就去了解了一下。真的是,日用而不知。

Mustache

简单介绍一下我所知道的前端历史

前后端不分离

页面基本是静态页面,后端采用JSP,freemarker,jdea,babel等渲染框架对前端模板进行预编译。

前后分离

使用字符串拼接

前端获取数据以后,利用如下的集中拼接方式

  var data = {name:'孙悟空',age:19}
  var html = "
" + data.name +"
" document.getElementById('container').innerHTML = html

使用反引号

  var data = {name:'孙悟空',age:19}
  var html = `
${data.name}
` document.getElementById('container').innerHTML = html

遇到循环时候

  var html = ""
  var data = {student:[{name:'张三'},{name:'李四'},{name:'王五'}]}
  data.students.forEach(function(stu){
    html += "
  • " + item.name + "
  • " }) document.getElementById('student').innerHTML = html

    换一种写法: 使用join()方法, 或者 concat 方法等

      var html = ""
      var data = {student:[{name:'张三',age: 20},{name:'李四',age: 18},{name:'王五', age: 30}]}
      data.students.forEach(function(item){
        html += ["
  • " + item.name + "
  • ","
  • " + item.age + "
  • "].join(" ") }) document.getElementById('student').innerHTML = html

    使用 art-template 渲染模板

    
    
      var html = template('test', data);
      document.getElementById(‘content’).innerHTML = html;
    

    用 vue react等框架渲染

    再后来运用vue react 等框架以后的渲染模式大家应该很清楚,这里就不再阐述了

    mustache的用法

    举个例子:

    var templateStr =`
        
    {{name}}
      {{#students}}
    1. 学生{{name}}的爱好是
        {{#hobbies}}
      1. {{.}}
      2. {{/hobbies}}
    2. {{/students}}
    var data = { name: '齐天大圣', students: [ {name:'小明', hobbies: ['游泳','健身']}, {name:'小红', hobbies: ['足球','篮球', '羽毛球']}, {name:'小强', hobbies: ['吃饭','睡觉']} ] }

    对于上述的js模板,通过mustache处理以后就会变成

    齐天大圣
    1. 学生小明的爱好是
      1. 游泳
      2. 健身
    2. 学生小红的爱好是
      1. 足球
      2. 篮球
      3. 羽毛球
    3. 学生小强的爱好是
      1. 吃饭
      2. 睡觉

    是不是很像vue react中的语法,可以想象如今框架肯定借鉴了这会写法,并把它加以改进,发扬光大。

    逻辑分析

    1. 对于简单的模板,我们可以用正则表达式进行实现

      例如下面的简单的:

      模板字符串如下:

    我买了一个{{thing}},我觉得好{{mood}}

    数据如下:

    { thing: '华为手机', mood: '开心' }
    

    实现方式如下:

    var data = { thing: '华为手机', mood: '开心' }
    var result = '

    我买了一个{{thing}},我觉得好{{mood}}

    '.replace(/\{\{(\w+)\}\}/g, function(match, $1){ // $1 分别是 thing mood return data[$1] }) console.log(result) //

    我买了一个华为手机,我觉得好开心

    1. 但是当情况复杂时候,例如循环时候或者判断时候,正则思路就不行了,

    tips: 模板字符串如下(其中.代表展开)

      {{#arr}}
    • {{.}}
    • {{/arr}}

    数据如下

    { arr: ["香蕉","苹果","橘子","西瓜"] }
    

    原理分析

    mustache 的渲染步骤分为了两步

    步骤如下:

    var tokens =  parseTemplateToTokens(templateStr)
    // 调用 renderTemplate 函数,让tokens 数组变成 dom 字符串
    var domHtml = renderTemplate(tokens, data)
    

    对于如下模板,渲染步骤:

    {{name}}
      {{#students}}
    1. 学生{{name}}的爱好是
        {{#hobbies}}
      1. {{.}}
      2. {{/hobbies}}
    2. {{/students}}
    1. 将模板渲染位 tokens 数组,结构类似于


      image-20210626212151391.png
    2. 将 tokens 数组转换为相应的 html,(结合data)

      var data = {
        name: '齐天大圣',
        students: [
          { name: '小明', hobbies: ['游泳', '健身'] },
          { name: '小红', hobbies: ['足球', '篮球', '羽毛球'] },
          { name: '小强', hobbies: ['吃饭', '睡觉'] }
        ]
      }
      

      转变的 html 结果如下

        
      齐天大圣
      1. 学生小明的爱好是
        1. 游泳
        2. 健身
      2. 学生小红的爱好是
        1. 足球
        2. 篮球
        3. 羽毛球
      3. 学生小强的爱好是
        1. 吃饭
        2. 睡觉

    代码实现

    模板变量如下

    1. 实现 parseTemplateToTokens 函数

      1. 书写一个扫描类,遍历字符串模板,里面有两个方法,一个是开始扫描,一个是扫描截止

        ①:跳过某个字符的扫描方法: 接受一个参数,当尾巴模板是以这个 参数 为处理更新当前指针和剩余字符串模板,比如 参数为 {{ , 就需要把当前指针向后移动两位({{的长度),并且 尾巴字符串 也要进行相应截取

        ②:扫描截止方法:接受一个参数,进行循环,当循环到当前参数字符串时候,就停止,并且返回开始循环到停止循环时中间的字符串。 例如当第一次扫描到 {{ 时,返回从开始位置到当前位置之间的字符串;接着扫描指针移动 {{ 的位置,再次调用,遇到 }},返回当前扫描指针到 }} 的字符,那就是{{ 和 }} 中间的变量,

        ③:当前再加一个方法:指针位置是否已经到最后了,返回值是一个布尔值

          class Scanner {
            constructor(templateStr){
              // 指针
              this.pos = 0
              // 尾巴,一开始就是模板字符串原文
              this.tail = templateStr
              this.templateStr = templateStr
            }
          
            scan(tag){
              if(this.tail.indexOf(tag) == 0){
                // tag 有多长,比如 {{ 长度是2,就让指针后移动几位
                this.pos += tag.length
                this.tail = this.templateStr.substr(this.pos)
              }
            }
          
            // 让指针进行扫描 直到遇到指定内容结束,并且能够返回结束之前路过的文字
            scanUtil(stopTag){
              // 记录一下开始的位置
              var 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
            }
          }
    
      返回哈哈哈哈
    
    1. 完成 parseTemplateToTokens 函数

      分析: 接受一个参数:当前字符串模板,利用 Scanner 进行处理,刚开始:指针从0开始,剩余的模板字符串(也称为尾巴)为当前所有字符串。首先调用遍历到 {{ 位置的方法,获得 {{ 前面的字符串,并push到一个数组中,以及更新指针和剩余的字符串。然后调用跳过扫描 {{ 的方法更新 当前指针 和 剩余模板。接着继续执行遍历到 }} 的位置,获得{{ 和 }} 之间的变量,push 到数组中,接着调用跳过 {{ 的方法,然后重复上述步骤,直到指针走到最后一位。
      tips: 当获得 {{ 和 }} 之间的字符串时,有可能是带有 # 或者 / 的这里需要进行特殊处理,往数组 push 时候增加相应类型以示区分。text 指静态文字,name 指的是 {{ 和 }} 之间不带(#、/)的变量,# 和 / 之后的变量也有进行记录

          function parseTemplateToTokens(templateStr){
            // 创建扫描器
              var scanner = new Scanner(templateStr)
              var tokens = []
              var word=""
              while(!scanner.eos()){
                word = scanner.scanUtil("{{")
                // 这里可以判断处理一下 空格问题,需要判断处理,例如 
  • 这里的空格就不能做处理 // 增加判断:空格是在 标签中的空格还是 标签间的空格 if(word){ let _word="" let isInnerTag = false for (let index = 0; index < word.length; index++) { const element = word[index]; if(element === "<"){ isInnerTag = true }else if(element === ">"){ isInnerTag = false } // 如果当前element 是空格,只有在 isInnerTag 为 true 时候才能加 if(/\s/.test(element)){ if(isInnerTag){ _word += element } }else{ _word += element } } tokens.push(['text', _word]) } scanner.scan("{{") word = scanner.scanUtil("}}") if(word){ if(word[0] === "#"){ // 存起来,从下标为1的项开始存取,因为下标为0的项是# tokens.push(['#', word.substr(1)]) }else if(word[0] === "/"){ tokens.push(['/', word.substr(1)]) }else{ tokens.push(['name', word]) } } scanner.scan("}}") } return tokens }
  •   以上获得了 tokens 数组,
      
      对于如下模板
    
          
    {{name}}
      {{#students}}
    1. 学生{{name}}的爱好是
        {{#hobbies}}
      1. {{.}}
      2. {{/hobbies}}
    2. {{/students}}
      获得到的tokens数组是这样的
    
    image.png

    然后还要处理里面的 # 和 / , 因为#和/ 是成对出现的,中间的内容应该是# 后面的子项。

    所以还需要一个处理上述tokens 的数组

          function nestToken(tokens){
            // 结果数组
            var nestTokens = []
            var sections = []
            // 收集器,收集子元素或者孙元素等,天生指向 nestTokens 数组,引用类型值,所以指向的是同一个数组
            // 收集器的指向会发生变化。当遇见# 时候,收集器会遇到 当前token 的下标为2的新数组,
            var collector = nestTokens
            var isFlag = true
            // 栈结构,存放小tokens, 栈顶(靠近端口的,最新进入的)tokens数组中前操作的这个tokens小数组
            tokens.forEach((token,index) => {
              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] : nestTokens
                  break
                default:
                  collector.push(token)
                  break
              }
            })
            return nestTokens
          }
    

    上面代码 精妙的地方就是声明了一个 收集器 collector 数组,当遇到 # 的时候,收集器要指向当前项目的下标为2的一项并且设置为数组,此后遍历的 token项是 被收集到收集器中,也就是token[2]中变为子项,并且有一个数组 sections push 当前token项;当遇到到 / 时候,对sections进行弹栈处理,并且进行判断处理,如果之前已经有过了#(sections数组length还不为0),那么收集器就指向sections栈顶的那一项的下标为2的数组,否则就代表是最外层,收集器指向最外层 nestTokens.

    经过上述函数处理以后的结果就是

    image.png

    完成 parseTemplateToTokens 和 nestToken 数组

    1. 实现 renderTemplate 函数

    经过上述分析,已经拿到了 带有嵌套关系的 数组结构

       function renderTemplate(tokens, data){
         var resultStr = ""
         for (let index = 0; index < tokens.length; index++) {
           const element = tokens[index];
           if(element[0] === "text"){
             resultStr +=element[1]
           }else if(element[0] === "name"){
             // 如果是name,说明是变量,需要对齐进行其他处理,因为可能是 a.b.c 
             resultStr += lookUp(data, element[1])
           }else if(element[0] === "#"){
             // 对于数组要进行解析处理,需要循环然后调用 renderTemplate 方法
             resultStr += parseArray(element[2], data[element[1]])
           }
         }
         return resultStr
       }
       
       // 处理 数组中 name 为 a.b.c 的变量
       function lookUp(dataObj, keyName){
         if(keyName.indexOf('.') !==-1 && keyName !== "."){
           var temp = dataObj
           var keys = keyName.split('.')
           for (let index = 0; index < keys.length; index++) {
             const element = keys[index];
             temp = temp[keys[index]]
           }
           return temp
         }
         return dataObj[keyName]
       }
       
       function parseArray(token, array){
         var resultStr = ""
         array.forEach(item => {
           // 这里兼容 . 属性,否则会报错
           resultStr += renderTemplate(token, {
             ...item,
             '.': item
           })
         })
         return resultStr
       }
    

    完成

    至此完成了mustache 的初步解析,当然源码比之更为复杂精炼。这里只是介绍了其基本原理。

    资源参考

    Vue源码解析系列课程之mustache模板引擎

    你可能感兴趣的:(Mustache)