mustache的基本使用:
- 在vue中使用是由两个大括号将变量包裹起来
<p>{{name}}</p>
实现原理分析:
1. 将模板字符串转化为tokens,tokens是js的二维数组
- 比如以下的模板字符串
<p>{{group.name}}成员的基本信息</p>
<ul>
{{#arr}}
<br>
<li>姓名:{{name}}</li>
<ul>爱好:
{{#hobbies}}
<ol>{{.}}</ol>
{{/hobbies}}
</ul>
{{/arr}}
</ul>
{{#arr}}与{{/arr}}之间,是需要遍历arr,{{#hobbies}}与{{/hobbies}}也是一个道理
- 转换为tokens是这样的
[
['text', '\n <p>'], // text表示是正常字符串
['name', 'group.name'], // name 表示'group.name'是变量
['text', '成员的基本信息</p>\n <ul>\n '],
['#', 'arr', [ // '#'表示要遍历后面那个变量'arr'
['text', '\n <br>\n <li>姓名:'],
['name', 'name'],
['text', '</li>\n <ul>爱好: \n '],
['#', 'hobbies',[ // 这里也要遍历'hobbies'
['text', '\n <ol>'],
['name', '.'], // 遍历的是数组,所以变量名是点
['text', '</ol>\n ']
]],
['text', '\n </ul>\n ']
]],
['text', '\n </ul>\n']
]
2. 数据与 tokens 结合解析为 dom 字符串。
遍历tokens,结合数据,将tokens转换为DOM字符串
- 比如以下数据,结合上面的tokens
let data = {
group: {
name: "三组"
},
arr: [
{ name: "张三", hobbies: ["篮球", "足球"] },
{ name: "李四", hobbies: ["拼图", "看电视剧", "购物"] },
{ name: "王五", hobbies: ["打游戏"] },
{ name: "赵六", hobbies: ["跑步", "股票"] }
]
}
- 解析为以下的dom字符串
<p>三组成员的基本信息</p>
<ul>
<br>
<li>姓名:张三</li>
<ul>爱好:
<ol>篮球</ol>
<ol>足球</ol>
</ul>
<br>
<li>姓名:李四</li>
<ul>爱好:
<ol>拼图</ol>
<ol>看电视剧</ol>
<ol>购物</ol>
</ul>
<br>
<li>姓名:王五</li>
<ul>爱好:
<ol>打游戏</ol>
</ul>
<br>
<li>姓名:赵六</li>
<ul>爱好:
<ol>跑步</ol>
<ol>股票</ol>
</ul>
</ul>
实现代码:
- 本案例主要以mustache库为原型,手写胡子语法,非Vue的mustache。
第一步:将模板字符串转换为Tokens
scanner.js
- 用于扫描模板字符串,找到tag内的变量,与tag外的变量
// 扫描器
export default class Scanner {
constructor(template) {
this.template = template
this.pos = 0 // 记录当前扫描的字符串的索引
this.tail = template // 记录剩下未扫描的字符串,刚开始是模板字符串
}
// 跳过标记
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
this.pos += tag.length // 跳过标记
this.tail = this.template.slice(this.pos) // 重新获取剩下的字符串
}
}
// 扫描直到标记
scanUtil(tag) {
let pos_start = this.pos // 记录从哪开始扫描
// 如果未扫描到标记就继续扫描,直到标记
while (!this.eos() && this.tail.indexOf(tag) !== 0) {
this.pos++
this.tail = this.template.slice(this.pos)
}
return this.template.slice(pos_start, this.pos) // 返回标记前的所有字符
}
// end of string 判断是否扫描结束
eos() {
return this.pos >= this.template.length
}
}
getToken.js
- 循环使用扫描器,直到将模板字符串全部扫描结束
import Scanner from "./scanner" // 引入扫描器
import nestToken from "./nestToken" // 整合扫描后的tokens
// 将字符串转换为tokens
export default function getTokens(template) {
let tokens = []
let word // 记录每次扫描后的结果
// 实例化扫描器
let scanner = new Scanner(template)
// 循环扫描,直到扫描完整个模板字符串
while (!scanner.eos()) {
word = scanner.scanUtil("{{") // 获取“{{”前的字符串
tokens.push(["text", word]) // “{{”前的字符串是text
scanner.scan("{{")
if (!scanner.eos()) {
word = scanner.scanUtil("}}") // 获取括号内的字符串
if (word[0] == "#") { // 如果括号内第一个字符是#号,表示要循环,单独处理
tokens.push(["#", word.slice(1)])
} else if (word[0] == "/") { // 如果括号内第一个字符是/,表示循环结束,单独处理
tokens.push(["/", word.slice(1)])
} else {
tokens.push(["name", word]) // 其他情况,就是name
}
scanner.scan("}}")
}
}
return nestToken(tokens) // 整合扫描后得到的tokens
}
nestToken.js
- 整合折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
// 折叠tokens
export default function nestToken(tokens) {
let nestToken = []
let sections = [] // 栈
let collector = nestToken // 收集器,默认指向nestToken
// 循环tokens
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
switch (token[0]) {
case "#": // 如果第一个参数是#,则需要折叠
// 入栈
sections.push(token)
// 收集
collector.push(token)
// 改变收集器指向
collector = token[2] = []
break;
case "/":
// 出栈
sections.pop()
// 改变收集器执行,如果栈中还有数据,指向最后一个,栈中没有数据的话指向nestToken
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestToken
break;
default:
collector.push(token)
break;
}
}
return nestToken
}
第二步:数据结合tokens解析为Dom字符串
margeTokenAndData.js
- 让tokens数组变为dom字符串
import lookdata from "./lookdata"; // 引入对象查找方法
// 将tokens转换为DOM字符串
export default function margeTokenAndData(tokens, data) {
let resultStr = ""
// 循环tokens
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token[0] == "text") { // 第一个是text不做处理
resultStr += token[1]
} else if (token[0] == "name") { // 第一个是name,需要赋值
resultStr += lookdata(data, token[1])
} else if (token[0] == "#") { // 第一个是#号,需要循环数组
let arr = lookdata(data, token[1])
// 循环数组,并且递归转换模板字符串
arr.forEach(obj => {
resultStr += margeTokenAndData(token[2], obj)
});
}
}
return resultStr
}
lookdata.js
- 查找对象的某个属性
- 解决以下两个问题:
- obj[a.b.c],无法获取的问题
- 如果是遍历数组,mustache中是{{.}},就是直接返回这个obj
export default function lookdata(obj, parms) {
if (parms == ".") return obj // 如果参数只有一个点,直接返回该对象
if (parms.includes(".")) { // 参数如果是a.b.c,则分割参数后,依次获取
let arr = parms.split(".")
return arr.reduce((pre, item) => pre[item], obj)
}
return obj[parms] // 如果参数不满足以上逻辑,则可以返回obj[parms]
}
第三步:向外暴露模板引擎与其render方法
index.js
- 整合以上方法,向外暴露模板引擎与其render方法
import getTokens from "./getToken"
import margeTokenAndData from "./margeTokenAndData"
// 挂载到window对象上向外暴露
window.ylzTE = {
render(template, data) {
let tokens = getTokens(template) // 获取tokens
return margeTokenAndData(tokens, data) // 将tokens与数据结合为dom字符串
}
}
/*-----------------------------------------------------------------------------*/
// 测试代码
let template = `
<h1>
<p>{{group.name}}成员的基本信息</p>
<ul>
{{#arr}}
<br>
<li>姓名:{{name}}</li>
<li>年龄:{{age}}</li>
<li>性别:{{sex}}</li>
<ul>爱好:
{{#hobbies}}
<ol>{{.}}</ol>
{{/hobbies}}
</ul>
{{/arr}}
</ul>
</h1>
`
let data = {
group: {
name: "三组"
},
arr: [
{ name: "张三", age: 19, sex: "男", hobbies: ["篮球", "足球"] },
{ name: "李四", age: 23, sex: "女", hobbies: ["拼图", "看电视剧", "购物"] },
{ name: "王五", age: 43, sex: "男", hobbies: ["打游戏"] },
{ name: "赵六", age: 22, sex: "男", hobbies: ["跑步", "股票"] }
]
}
const box = document.getElementById("box")
box.innerHTML = ylzTE.render(template, data)