是什么?
Mustache 英文翻译为胡子,这个库的名字听起来很抽象,看到下面代码同学们就知道为什么这个库的名字叫胡子了。
<ul>
{{#arr}}
<li>
<div>{{name}}</div>
<p>{{age}}</p>
<p>{{sex}}</p>
</li>
{{/arr}}
</ul>
{{ }},两个大括号在使用的时候是不是很像两撮胡子,这个库可以说是最早将模板思想带给前端的库,包括Vue也是借鉴了该库,对同学们来说有非常好的学习意义。
Mustache的github地址: https://github.com/janl/mustache.js
怎么用?
Mustache的使用方式其实并不难,我们这期将会讲解循环dom和dom隐藏显示是如何实现的,首先我们要是想使用这个库需要先定义一个模板template。
const template = `
<ul>
{{#arr}}
<li>
<div>{{ name}}</div>
<p>{{ age}}</p>
<p>{{ sex}}</p>
</li>
{{/arr}}
</ul>
<div>{{thing}}</div>
{{#simpleArr}}
<div>{{.}}</div>
{{/simpleArr}}
{{#flag}}
<div>我是显示隐藏</div>
{{ /flag}}
<div>今天的气温{{weather.temperature}}, 风力为{{weather.windy}},
{{ weather.date.time }}</div>
`
接下来定义一组模板需要渲染的数据。
const data = {
arr: [
{name: '小白', age: 18, sex: '男'},
{name: '小花', age: 32, sex: '女'},
{name: '小宏', age: 21, sex: '未知'}
],
thing: '一些事务',
simpleArr: ['狗', '猫', '鸡'],
flag: false,
weather: {
temperature: '18摄氏度',
windy: 2,
date: {
time: '2024-3-15'
}
}
}
最后通过Mustache库的render函数返回结合后的dom字符串,我们将dom字符串挂载到页面中即可。如下:
const container = document.querySelector('#app')
let result = Mustache.render(template, data)
container.innerHTML = result
上述就是Mustache的使用流程,其中在两撮胡子中间的数据 {{ }} 就是取自data中的数据,{{#arr}} {{/arr}},这两个标记中间的dom字符串将会被循环渲染数组中的数据,{{#flag}} {{/flag}}这两个标记中间的dom字符串,如果flag的值为false将会隐藏该标记中间的字符串,若是true则显示其中的字符串。
如何实现?
1. 将template转为tokens
第一步就是将定义的模板转换为一个个的token,token就是对模板的拆解,是一个二维数组,我们这期的博客中只实现四种token的类型,
第一种为text类型的token
除了{{ }}中的内容以外其它都为text类型的token,如:['text', '<ul>'],['text', '<li> <div>']。
第二种为name类型的token
{{ }}中若是只有一个简单的字段名且该字段名前面无任何特殊标记的被分为name类型的token,如:['name', 'things']
第三种为#类型的token
{{ }} 中若是第一字符为#后面跟着字段则是#类型的token,如:['#', arr, [.......]], ['#', ‘flag’, [.......]]
第四种为/类型的token
/ 类型的token是和 # 类型的token相对的,类似于html标签,# 类型的token后面必须有 / 类型的token作为结尾,如:['/', arr], ['/', ‘flag’]
接下来将会为同学们展示如何将template转换为这四类token并放入tokens数组中。
- 首先先定义一个Scanner类,该类是一个辅助类包含三个方法,ScanUntil, Scan, eos,其中ScanUntil传入一个实参tag,该函数的功能是扫描字符串直到遇到tag标记并将扫描过的部分返回,Scan传人一个实参tag,该函数的作用是当ScanUntil函数遇到tag标记停下来后辅助跳过tag,eos函数是判断模板是否已经扫描完了。 算法思路:在类中定义两个成员变量 template, tailTemplateStr,和 在类的构造函数中传入template并赋值给这两个成员变量,在ScanUntil函数中通过字符串的indexOf()函数来查找tag,定义一个索引i,进行while循环,若indexOf()返回的不是0,i自增,截取tailTemplateStr将其重新复制给tailTemplateStr。在Scan函数中只需要操作tailTemplateStr字符串截取出tag以外的部分即可,eos函数判断tailTemplateStr是否为空串即可。
- 接下来定义一个类ParseTemplate,首先定义一个方法squashTokens,这个方法会将template转为零散的tokens,在这个函数中会对模板进行遍历并返回相应的值生成token将其组合成零散的tokens。
- 最后在该类中定义一个nestTokens方法,在这个方法中会将零散的tokens转为折叠的tokens,也就是将扁平化的数组组成树结构的数组。 算法思路:自定义一个栈的数据结构,在方法中定义一个结果tokens的常量resultTokens,收集器变量collectors,初始化栈sections,首先将collectors的指针指向resultTokens,遍历tokens,若该token是 # 类型的,首先将该token push进collectors中(因为指针指向,相当于resultTokens也push进了token),再将该token推入sections栈,并将collectors指向该token索引为2的项并初始化为空数组,若遍历到的tokens为 / 类型,secitons先出栈,在判断栈是否为空,若为空,collectors重新指回resultTokens(栈为空则代表要重新收集树节点的内容),若不为空,collectors指向栈顶token的索引第二位(收集叶子节点的内容),如果不是上述两种情况,直接将token push进collectors,最后将resultTokens返回即可。建议将nestTokens方法debugger可以更好地理解指针收集器的原理
Scanner类,ParseTemplate类和栈结构代码如下:
// 扫描器,mustache中用于检测{{ }}
class Scanner {
template = ''
tailTemplateStr = ''
constructor(template) {
this.template = template
this.tailTemplateStr = template
}
// 跳过指定标签
scan(tag) {
for (let i = 0; i < tag.length; i++) {
if(this.tailTemplateStr.length > 0) {
this.tailTemplateStr = this.tailTemplateStr.substring(1)
}
}
}
// 寻找相应标记前的字符串 将其返回
scanUntil(tag) {
let i = 0
let originTailTemplateStr = this.tailTemplateStr
while(this.tailTemplateStr.indexOf(tag) !== 0 && this.tailTemplateStr.length > 0) {
i++
this.tailTemplateStr = this.tailTemplateStr.substring(1)
}
return originTailTemplateStr.substring(0, i)
}
// 是否走到底了
eos() {
return this.tailTemplateStr.length === 0
}
}
// 解析模板为token
class ParseTemplate {
template = ''
tokens = []
scanner = null
constructor(template) {
this.template = template
this.scanner = new Scanner(template)
this.squashTokens()
}
// 模板转为零散token
squashTokens() {
while(!this.scanner.eos()) {
let word1 = this.scanner.scanUntil("{{").trim()
if(word1) {
this.tokens.push(['text', word1])
}
this.scanner.scan("{{")
let word2 = this.scanner.scanUntil("}}").trim()
if(TAG.includes(word2.charAt(0))) {
this.tokens.push([word2.charAt(0), word2.substring(1).trim()])
} else if (word2) {
this.tokens.push(['name', word2])
}
this.scanner.scan("}}")
}
}
/**
* 折叠tokens
* 通过收集器指针引用的方式
*/
nestTokens(tokens) {
const nestTokens = []
// 栈结构放置 # /的token
const sections = new Stack()
// 收集器
let collectors = nestTokens
tokens.forEach(token=>{
switch(token[0]) {
case '#':
// push进收集器中,收集器指针指向结果数组,结果数组相当于也push进去了
collectors.push(token)
// 入栈,遇到 / 出栈
sections.push(token)
// 收集器指向该token的下标为二的项,为接下来收集该数组里面的内容做准备
collectors = token[2] = []
break
case '/':
sections.pop()
collectors = sections.isEmpty() ? nestTokens : sections.peek()[2]
break
default:
collectors.push(token)
}
})
return nestTokens
}
// 获取tokens
getTokens() {
return this.nestTokens(this.tokens)
}
}
// 栈
class Stack {
arr = []
// 入栈
push(ele) {
this.arr.push(ele)
}
// 出栈
pop() {
if(this.isEmpty()) {
return undefined
}
return this.arr.pop()
}
// 栈为空
isEmpty() {
return this.arr.length === 0
}
// 查看栈顶元素
peek() {
if(this.isEmpty()) {
return undefined
}
return this.arr[this.arr.length - 1]
}
}
2. 将tokens和data结合转为template
将tokens转为template用到了递归的方法,我们先定义一个函数parse,该函数传入一个tokens和一个data,首先我们先正常的遍历tokens,如果遇到text类型的token,直接加到结果字符串中,若遇到了name类型的token,我们不能直接给结果字符串追加一个data[token[1]]的数据,因为可能会有 xxx.xxx.xx 进行点式调用对象里面的数据,如果直接data[]里面加上字段名会返回undefined,我们需要写一个辅助函数,该函数的作用是传入对象和键名返回对应的数据,若遇到 # 类型的token 我们首先判断该类型在data中是不是数组类型,若不是,判断是不是true,如果不是true不做任何处理,如果是true,我们调用parse,方法并将该token下标为2的项和data传进去,若 # 类型的token 在data中是数组类型,则调用recursionArr方法,recursionArr方法同样接受两个参数,第一个参数为tokens,第二个参数为数组,在该函数内部,循环数组并调用parse方法,将tokens和数组的每一项传入parse中。最后在parse中返回累加的结果
代码如下:
// 将tokens 和 data 组成 template
class TokensToTemplate {
tokens = []
data = {}
constructor(tokens, data) {
this.data = data
this.tokens = tokens
}
parse(tokens, data) {
let result = ''
if(!Array.isArray(tokens)) {
return result
}
tokens.forEach(token=>{
if(token[0] === 'text') {
result += token[1]
} else if(token[0] === 'name') {
result += Lib.findObj(data, token[1])
} else {
if(Array.isArray(data[token[1]])) {
result += this.recursionArr(token[2], data[token[1]])
} else {
if(data[token[1]]) {
result += this.parse(token[2], data)
}
}
}
})
return result
}
// 递归数组转为template
recursionArr(tokens, data) {
let result = ''
data.forEach((v, i)=>{
console.log()
result += this.parse(tokens, {
...v,
'.': v
})
})
return result
}
}
// 工具类型
class Lib {
static findObj(data, keyName) {
let obj = data
if(keyName.indexOf('.') === -1 || keyName === '.') {
return data[keyName]
}
keyName.split('.').forEach(key=>{
obj = obj[key]
})
return obj
}
}
总结
以上就是Mustache库核心内容的实现,在Mustahce库的源码中还有其它不同类型的token实现,有兴趣的小伙伴可以去github上研究下源码。