模板引擎:将数据变为视图的最优雅的解决方案
模板引擎的发展:
实现代码:
本案例主要实现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)