Mustache

mustache: 中文意思是:髭;上唇的胡子;長(zhǎng)髭

它是一款經(jīng)典的前端模板引擎,在前后端分離的技術(shù)架構(gòu)下面,一度流行。之前也用過 art-template 之類的模板插件,應(yīng)該也是同樣的原理。如今隨著 前端三大框架 的流行,這種方式已經(jīng)深入前端人心。但是我還是第一次聽到這個(gè)框架,就去了解了一下。真的是,日用而不知。

Mustache

簡(jiǎn)單介紹一下我所知道的前端歷史

前后端不分離

頁面基本是靜態(tài)頁面,后端采用JSP,freemarker,jdea,babel等渲染框架對(duì)前端模板進(jìn)行預(yù)編譯。

前后分離

使用字符串拼接

前端獲取數(shù)據(jù)以后,利用如下的集中拼接方式

  var data = {name:'孫悟空',age:19}
  var html = "<div>" + data.name +"</div>"
  document.getElementById('container').innerHTML = html

使用反引號(hào)

  var data = {name:'孫悟空',age:19}
  var html = `<div>${data.name}</div>`
  document.getElementById('container').innerHTML = html

遇到循環(huán)時(shí)候

  var html = ""
  var data = {student:[{name:'張三'},{name:'李四'},{name:'王五'}]}
  data.students.forEach(function(stu){
    html += "<li>" + item.name + "</li>"
  })
  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 += ["<li>" + item.name + "</li>","<li>" + item.age + "</li>"].join(" ")
  })
  document.getElementById('student').innerHTML = html

使用 art-template 渲染模板

<script id=”test” type=”text/html”>
  <div>
    <div class="mine">{{name}}</div>
    <ol id="me" style="color: red">
      {{#students}}
        <li>
          學(xué)生{{name}}的愛好是
          <ol>
            {{#hobbies}}
              <li>{{.}}</li>
            {{/hobbies}}
          </ol>
        </li>
      {{/students}}
    </ol>
  </div>
</script>
  var html = template('test', data);
  document.getElementById(‘content’).innerHTML = html;

用 vue react等框架渲染

再后來運(yùn)用vue react 等框架以后的渲染模式大家應(yīng)該很清楚,這里就不再闡述了

mustache的用法

舉個(gè)例子:

var templateStr =`
    <div>
      <div class="mine">{{name}}</div>
      <ol id="me" style="color: red">
        {{#students}}
          <li>
            學(xué)生{{name}}的愛好是
            <ol>
              {{#hobbies}}
                <li>{{.}}</li>
              {{/hobbies}}
            </ol>
          </li>
        {{/students}}
      </ol>
    </div>
  var data = {
    name: '齊天大圣',
    students: [
      {name:'小明', hobbies: ['游泳','健身']},
      {name:'小紅', hobbies: ['足球','籃球', '羽毛球']},
      {name:'小強(qiáng)', hobbies: ['吃飯','睡覺']}
    ]
  }

對(duì)于上述的js模板,通過mustache處理以后就會(huì)變成

<div>
    <div class="mine">齊天大圣</div>
    <ol id="me" style="color: red">
      <li>學(xué)生小明的愛好是<ol>
          <li>游泳</li>
          <li>健身</li>
        </ol>
      </li>
      <li>學(xué)生小紅的愛好是<ol>
          <li>足球</li>
          <li>籃球</li>
          <li>羽毛球</li>
        </ol>
      </li>
      <li>學(xué)生小強(qiáng)的愛好是<ol>
          <li>吃飯</li>
          <li>睡覺</li>
        </ol>
      </li>
    </ol>
  </div>

是不是很像vue react中的語法,可以想象如今框架肯定借鑒了這會(huì)寫法,并把它加以改進(jìn),發(fā)揚(yáng)光大。

邏輯分析

  1. 對(duì)于簡(jiǎn)單的模板,我們可以用正則表達(dá)式進(jìn)行實(shí)現(xiàn)

    例如下面的簡(jiǎn)單的:

    模板字符串如下:

<h1>我買了一個(gè){{thing}},我覺得好{{mood}}</h1>

數(shù)據(jù)如下:

{ thing: '華為手機(jī)', mood: '開心' }

實(shí)現(xiàn)方式如下:

var data = { thing: '華為手機(jī)', mood: '開心' }
var result = '<h1>我買了一個(gè){{thing}},我覺得好{{mood}}</h1>'.replace(/\{\{(\w+)\}\}/g, function(match, $1){
  // $1 分別是 thing mood
  return data[$1]
})
console.log(result) // <h1>我買了一個(gè)華為手機(jī),我覺得好開心</h1>
  1. 但是當(dāng)情況復(fù)雜時(shí)候,例如循環(huán)時(shí)候或者判斷時(shí)候,正則思路就不行了,

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

<ul>
  {{#arr}}
    <li>{{.}}</li>
  {{/arr}}
</ul>

數(shù)據(jù)如下

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

原理分析

mustache 的渲染步驟分為了兩步

步驟如下:

var tokens =  parseTemplateToTokens(templateStr)
// 調(diào)用 renderTemplate 函數(shù),讓tokens 數(shù)組變成 dom 字符串
var domHtml = renderTemplate(tokens, data)

對(duì)于如下模板,渲染步驟:

<div>
   <div class="mine">{{name}}</div>
      <ol id="me" style="color: red">
        {{#students}}
          <li>
            學(xué)生{{name}}的愛好是
            <ol>
              {{#hobbies}}
                <li>{{.}}</li>
              {{/hobbies}}
            </ol>
          </li>
        {{/students}}
      </ol>
    </div>
 </div>
  1. 將模板渲染位 tokens 數(shù)組,結(jié)構(gòu)類似于


    image-20210626212151391.png
  2. 將 tokens 數(shù)組轉(zhuǎn)換為相應(yīng)的 html,(結(jié)合data)

    var data = {
      name: '齊天大圣',
      students: [
        { name: '小明', hobbies: ['游泳', '健身'] },
        { name: '小紅', hobbies: ['足球', '籃球', '羽毛球'] },
        { name: '小強(qiáng)', hobbies: ['吃飯', '睡覺'] }
      ]
    }
    

    轉(zhuǎn)變的 html 結(jié)果如下

      <div>
        <div class="mine">齊天大圣</div>
        <ol id="me" style="color: red">
          <li>學(xué)生小明的愛好是<ol>
              <li>游泳</li>
              <li>健身</li>
            </ol>
          </li>
          <li>學(xué)生小紅的愛好是<ol>
              <li>足球</li>
              <li>籃球</li>
              <li>羽毛球</li>
            </ol>
          </li>
          <li>學(xué)生小強(qiáng)的愛好是<ol>
              <li>吃飯</li>
              <li>睡覺</li>
            </ol>
          </li>
        </ol>
      </div>
    

代碼實(shí)現(xiàn)

模板變量如下

  1. 實(shí)現(xiàn) parseTemplateToTokens 函數(shù)

    1. 書寫一個(gè)掃描類,遍歷字符串模板,里面有兩個(gè)方法,一個(gè)是開始掃描,一個(gè)是掃描截止

      ①:跳過某個(gè)字符的掃描方法: 接受一個(gè)參數(shù),當(dāng)尾巴模板是以這個(gè) 參數(shù) 為處理更新當(dāng)前指針和剩余字符串模板,比如 參數(shù)為 {{ , 就需要把當(dāng)前指針向后移動(dòng)兩位({{的長(zhǎng)度),并且 尾巴字符串 也要進(jìn)行相應(yīng)截取

      ②:掃描截止方法:接受一個(gè)參數(shù),進(jìn)行循環(huán),當(dāng)循環(huán)到當(dāng)前參數(shù)字符串時(shí)候,就停止,并且返回開始循環(huán)到停止循環(huán)時(shí)中間的字符串。 例如當(dāng)?shù)谝淮螔呙璧?{{ 時(shí),返回從開始位置到當(dāng)前位置之間的字符串;接著掃描指針移動(dòng) {{ 的位置,再次調(diào)用,遇到 }},返回當(dāng)前掃描指針到 }} 的字符,那就是{{ 和 }} 中間的變量,

      ③:當(dāng)前再加一個(gè)方法:指針位置是否已經(jīng)到最后了,返回值是一個(gè)布爾值

      class Scanner {
        constructor(templateStr){
          // 指針
          this.pos = 0
          // 尾巴,一開始就是模板字符串原文
          this.tail = templateStr
          this.templateStr = templateStr
        }
      
        scan(tag){
          if(this.tail.indexOf(tag) == 0){
            // tag 有多長(zhǎng),比如 {{ 長(zhǎng)度是2,就讓指針后移動(dòng)幾位
            this.pos += tag.length
            this.tail = this.templateStr.substr(this.pos)
          }
        }
      
        // 讓指針進(jìn)行掃描 直到遇到指定內(nèi)容結(jié)束,并且能夠返回結(jié)束之前路過的文字
        scanUtil(stopTag){
          // 記錄一下開始的位置
          var POS_BACKUP = this.pos
          // 當(dāng)尾巴的開頭不是 stopTag 的時(shí)候,說明還沒有掃描到 stopTag
          while(!this.eos() && this.tail.indexOf(stopTag) != 0){
            this.pos++
            // 改變尾巴,從當(dāng)前指針這個(gè)字符開始到最后的全部字符
            this.tail = this.templateStr.substring(this.pos)
          }
          // 返回當(dāng)前截取到的字符串
          return this.templateStr.substring(POS_BACKUP, this.pos)
        }
      
        // 指針是否到頭,返回布爾值
        eos(){
          return this.pos >= this.templateStr.length
        }
      }
  返回哈哈哈哈
  1. 完成 parseTemplateToTokens 函數(shù)

    分析: 接受一個(gè)參數(shù):當(dāng)前字符串模板,利用 Scanner 進(jìn)行處理,剛開始:指針從0開始,剩余的模板字符串(也稱為尾巴)為當(dāng)前所有字符串。首先調(diào)用遍歷到 {{ 位置的方法,獲得 {{ 前面的字符串,并push到一個(gè)數(shù)組中,以及更新指針和剩余的字符串。然后調(diào)用跳過掃描 {{ 的方法更新 當(dāng)前指針 和 剩余模板。接著繼續(xù)執(zhí)行遍歷到 }} 的位置,獲得{{ 和 }} 之間的變量,push 到數(shù)組中,接著調(diào)用跳過 {{ 的方法,然后重復(fù)上述步驟,直到指針走到最后一位。
    tips: 當(dāng)獲得 {{ 和 }} 之間的字符串時(shí),有可能是帶有 # 或者 / 的這里需要進(jìn)行特殊處理,往數(shù)組 push 時(shí)候增加相應(yīng)類型以示區(qū)分。text 指靜態(tài)文字,name 指的是 {{ 和 }} 之間不帶(#、/)的變量,# 和 / 之后的變量也有進(jìn)行記錄

      function parseTemplateToTokens(templateStr){
        // 創(chuàng)建掃描器
          var scanner = new Scanner(templateStr)
          var tokens = []
          var word=""
          while(!scanner.eos()){
            word = scanner.scanUtil("{{")
            // 這里可以判斷處理一下 空格問題,需要判斷處理,例如 <li class="red">這里的空格就不能做處理
            // 增加判斷:空格是在 標(biāo)簽中的空格還是 標(biāo)簽間的空格
            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
                }
                // 如果當(dāng)前element 是空格,只有在 isInnerTag 為 true 時(shí)候才能加
                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] === "#"){
                // 存起來,從下標(biāo)為1的項(xiàng)開始存取,因?yàn)橄聵?biāo)為0的項(xiàng)是#
                tokens.push(['#', word.substr(1)])
              }else if(word[0] === "/"){
                tokens.push(['/', word.substr(1)])
              }else{          
                tokens.push(['name', word])
              }
            } 
            scanner.scan("}}")
          }
          return tokens
      }
  以上獲得了 tokens 數(shù)組,
  
  對(duì)于如下模板
      <div>
         <div class="mine">{{name}}</div>
            <ol id="me" style="color: red">
              {{#students}}
                <li>
                  學(xué)生{{name}}的愛好是
                  <ol>
                    {{#hobbies}}
                      <li>{{.}}</li>
                    {{/hobbies}}
                  </ol>
                </li>
              {{/students}}
            </ol>
          </div>
      </div>
  獲得到的tokens數(shù)組是這樣的
image.png

然后還要處理里面的 # 和 / , 因?yàn)?和/ 是成對(duì)出現(xiàn)的,中間的內(nèi)容應(yīng)該是# 后面的子項(xiàng)。

所以還需要一個(gè)處理上述tokens 的數(shù)組

      function nestToken(tokens){
        // 結(jié)果數(shù)組
        var nestTokens = []
        var sections = []
        // 收集器,收集子元素或者孫元素等,天生指向 nestTokens 數(shù)組,引用類型值,所以指向的是同一個(gè)數(shù)組
        // 收集器的指向會(huì)發(fā)生變化。當(dāng)遇見# 時(shí)候,收集器會(huì)遇到 當(dāng)前token 的下標(biāo)為2的新數(shù)組,
        var collector = nestTokens
        var isFlag = true
        // 棧結(jié)構(gòu),存放小tokens, 棧頂(靠近端口的,最新進(jìn)入的)tokens數(shù)組中前操作的這個(gè)tokens小數(shù)組
        tokens.forEach((token,index) => {
          switch (token[0]) {
            case '#':
              // 收集器放入這個(gè)token
              collector.push(token)
              // 入棧
              sections.push(token)
              // 收集器要換人了, 給token 添加下標(biāo)為2的項(xiàng)目,并讓收集器指向它
              collector = token[2]= []
              break
            case '/':
              // 出棧 pop 會(huì)返回剛剛彈出的項(xiàng)
              sections.pop()
              // 改變收集器為棧結(jié)構(gòu)隊(duì)尾(隊(duì)尾就是棧頂) 那項(xiàng)下標(biāo)為2的數(shù)組
              collector = sections.length > 0 ? sections[sections.length-1][2] : nestTokens
              break
            default:
              collector.push(token)
              break
          }
        })
        return nestTokens
      }

上面代碼 精妙的地方就是聲明了一個(gè) 收集器 collector 數(shù)組,當(dāng)遇到 # 的時(shí)候,收集器要指向當(dāng)前項(xiàng)目的下標(biāo)為2的一項(xiàng)并且設(shè)置為數(shù)組,此后遍歷的 token項(xiàng)是 被收集到收集器中,也就是token[2]中變?yōu)樽禹?xiàng),并且有一個(gè)數(shù)組 sections push 當(dāng)前token項(xiàng);當(dāng)遇到到 / 時(shí)候,對(duì)sections進(jìn)行彈棧處理,并且進(jìn)行判斷處理,如果之前已經(jīng)有過了#(sections數(shù)組length還不為0),那么收集器就指向sections棧頂?shù)哪且豁?xiàng)的下標(biāo)為2的數(shù)組,否則就代表是最外層,收集器指向最外層 nestTokens.

經(jīng)過上述函數(shù)處理以后的結(jié)果就是

image.png

完成 parseTemplateToTokens 和 nestToken 數(shù)組

  1. 實(shí)現(xiàn) renderTemplate 函數(shù)

經(jīng)過上述分析,已經(jīng)拿到了 帶有嵌套關(guān)系的 數(shù)組結(jié)構(gòu)

   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,說明是變量,需要對(duì)齊進(jìn)行其他處理,因?yàn)榭赡苁?a.b.c 
         resultStr += lookUp(data, element[1])
       }else if(element[0] === "#"){
         // 對(duì)于數(shù)組要進(jìn)行解析處理,需要循環(huán)然后調(diào)用 renderTemplate 方法
         resultStr += parseArray(element[2], data[element[1]])
       }
     }
     return resultStr
   }
   
   // 處理 數(shù)組中 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 => {
       // 這里兼容 . 屬性,否則會(huì)報(bào)錯(cuò)
       resultStr += renderTemplate(token, {
         ...item,
         '.': item
       })
     })
     return resultStr
   }

完成

至此完成了mustache 的初步解析,當(dāng)然源碼比之更為復(fù)雜精煉。這里只是介紹了其基本原理。

資源參考

Vue源碼解析系列課程之mustache模板引擎

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 由于1.0以后變化都不是很大,所有就直接看最新的源碼解析了https://cdn.bootcdn.net/ajax...
    AAA前端閱讀 438評(píng)論 0 0
  • 源碼地址:https://cdn.bootcdn.net/ajax/libs/mustache.js/1.0.0/...
    AAA前端閱讀 649評(píng)論 0 0
  • 模板引擎之前的時(shí)代 純DOM法: 非常笨拙,沒有實(shí)戰(zhàn)價(jià)值 數(shù)組join法: 曾幾何時(shí)非常流行,是曾經(jīng)的前端必會(huì)知識(shí)...
    強(qiáng)某某閱讀 997評(píng)論 0 3
  • mustache.js 是一個(gè)簡(jiǎn)單強(qiáng)大的 JavaScript 模板引擎,最新版本:https://www.boo...
    雷雨leiyu閱讀 742評(píng)論 0 0
  • 一、簡(jiǎn)介 Web 模板引擎是為了使用戶界面與業(yè)務(wù)數(shù)據(jù)(內(nèi)容)分離而產(chǎn)生的,它可以生成特定格式的文檔,通常是標(biāo)準(zhǔn)的 ...
    華夏車前子閱讀 1,417評(píng)論 0 0

友情鏈接更多精彩內(nèi)容