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)光大。
邏輯分析
-
對(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>
- 但是當(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>
-
將模板渲染位 tokens 數(shù)組,結(jié)構(gòu)類似于
image-20210626212151391.png -
將 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)
模板變量如下
-
實(shí)現(xiàn) parseTemplateToTokens 函數(shù)
-
書寫一個(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
}
}
返回哈哈哈哈
-
完成 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ù)組是這樣的
然后還要處理里面的 # 和 / , 因?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é)果就是
完成 parseTemplateToTokens 和 nestToken 數(shù)組
- 實(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ù)雜精煉。這里只是介紹了其基本原理。