将数据要变为视图的最优雅的解决方案。
1.将模版解析为tokens数组
2.将tokens数组替换数据后还原回模版字符串
// 模版 let templateStr = ` <ul> {{#students}} <li class="123"> {{name}}爱好是: {{age}} <ol> {{#hobbies}} <li>{{.}}</li> {{/hobbies}} </ol> </li> {{/students}} </ul>` // 数据 const data = { students: [{ name: 'Yang', age: 23, hobbies: ['写代码', '吃肉', '减肥'] }, { name: 'ZGR', age: 18, hobbies: ['吃肉', '吃肉', '吃肉'] }, { name: 'test', age: 999, hobbies: ['唱', '跳', 'rap', '篮球'] }] } /** * 通过Mustache.render方法完成2步 * 1.把模版解析为tokens数组 * 2.把tokens数组拼接数据后合并为模版字符串 */ const domStr = Mustache.render(templateStr, data) // 把拼接好的模版字符串写进body标签 document.body.innerHTML = domStr
分析=>将模版解析为tokens数组
// 全局提供Mustache window.Mustache = { render(templateStr, data) { // 调用parseTemplateToTokens让模版字符串变成数组 const tokens = parseTemplateToTokens(templateStr) // 调用renderTemplate函数把tokens变为Dom字符串 return renderTemplate(tokens, data) } }
export default class Scanner { constructor(templateStr) { // 将模版字符串保存在自身 this.templateStr = templateStr // 指针 this.pointer = 0 // 尾巴 一开始就是模版字符串的内容 this.tail = templateStr } // 路过指定内容 scan(tag) { if (this.tail.indexOf(tag) === 0) { // tag有多长 往后移动多少位 跳过标记 this.pointer += tag.length // 改变尾巴 this.tail = this.templateStr.substring(this.pointer) } } // 让指针进行扫描 直到遇见指定内容结束并且返回结束之前扫描的文字 scanUntil(stopTag) { // 记录执行本方法时 pointer 的值 const pointerBackUp = this.pointer // 当尾巴的开头不是stopTag的时候,就说明还没有扫描到 while (!this.eos() && this.tail.indexOf(stopTag) !== 0) { this.pointer++ this.tail = this.templateStr.substring(this.pointer) } return this.templateStr.substring(pointerBackUp, this.pointer) } // 指针是否到头了 eos() { // 当前的指针大于等于templateStr的长度 说明已经到头了 return this.pointer >= this.templateStr.length } }
export default function parseTemplateToTokens(templateStr) { // tokens数组 const tokens = [] // 创建扫描器 const scanner = new Scanner(templateStr) // 标记前的内容 let words // 启动扫描器 while (!scanner.eos()) { // 收集开始标记 words = scanner.scanUntil('{{') // 不为空存储 words && tokens.push(['text', words]) // 跳过本次开始标记 scanner.scan('{{') // 收集结束标记 words = scanner.scanUntil('}}') // 不为空存储 if (words !== '') { // 如果以#开头 if (words[0] === '#') { // 存储类型为# 值为去掉#的字符串 tokens.push(['#', words.substring(1)]) } else if (words[0] === '/') { // 存储类型为/ 值为去掉/的字符串 tokens.push(['/', words.substring(1)]) } else { // 如果均不是 保存类型为 name tokens.push(['name', words]) } } // 跳过本次结束标记 scanner.scan('}}') } // 返回处理好的tokens数组 此时tokens是零散的数组 return nestTokens(tokens) }
export default function nestTokens(tokens) { // 结果数组 const nestedTokens = [] // 收集器 最初指向 结果数组 let collector = nestedTokens // 存放小tokens const sections = [] // 遍历零散数组 tokens.forEach(token => { // 判断token第0位 switch (token[0]) { case '#': // 收集器中放入这个token collector.push(token) // 入栈 sections.push(token) // 换收集器 给token第二项添加一个空数组 并且让收集器指向它 collector = token[2] = [] break case '/': // 出栈 sections.pop() // 改变收集器为栈结构队尾数组下标为2的数组 collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens break default : collector.push(token) break } }) // 返回结果 此时数据已经处理完毕 return nestedTokens }
此时已经实现了第一步把模版字符串变成tokens数组
分析=>将tokens数组解析回模版字符串
export default function renderTemplate(tokens, data) { // 结果字符串 let resultStr = '' // 遍历tokens tokens.forEach(token => { // 如果当前的类型是text if (token[0] === 'text') { // 直接+=第二位就好了 resultStr += token[1] // 如果当前的类型是name } else if (token[0] === 'name') { // 那么用lookUp拿出值 resultStr += lookUp(data, token[1]) } else if (token[0] === '#') { resultStr += parseArray(token, data) } }) return resultStr }
export default function lookUp(dataObj, keyName) { // 如果keyName有. 并且不能只有. if (keyName && keyName !== '.' && keyName.indexOf('.') !== -1) { // 分割字符串 const keys = keyName.split('.') // 临时变量保存数据 let temp = dataObj keys.forEach(key => { // 一层一层找出数据 temp = temp[key] ? temp[key] : '' }) // 返回数据 return temp } // 如果没有.符号 return dataObj[keyName] || '' }
export default function parseArray(token, data) { let v = lookUp(data, token[1]) let resultStr = '' v.forEach(item => { // 要判断. resultStr += renderTemplate(token[2], {...item, '.': item}) }) return resultStr }
此时模版字符串已经准备就绪 通过innerHTML写进body里就可以了