模板引擎:将数据变为视图的最优雅的解决方案
模板引擎的发展:
实现代码:
本案例主要实现mustache库,非Vue的mustache
第一步:将模板字符串转换为Tokens
scanner.js
用于扫描模板字符串,找到tag内的变量,与tag外的变量
// 扫描器export default class Scanner {constructor(template) {this.template = templatethis.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// 将字符串转换为tokensexport default function getTokens(template) {let tokens = []let word // 记录每次扫描后的结果// 实例化扫描器let scanner = new Scanner(template)// 循环扫描,直到扫描完整个模板字符串while (!scanner.eos()) {word = scanner.scanUtil("{{") // 获取“{{”前的字符串tokens.push(["text", word]) // “{{”前的字符串是textscanner.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的项
// 折叠tokensexport default function nestToken(tokens) {let nestToken = []let sections = [] // 栈let collector = nestToken // 收集器,默认指向nestToken// 循环tokensfor (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()// 改变收集器执行,如果栈中还有数据,指向最后一个,栈中没有数据的话指向nestTokencollector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokenbreak;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 = ""// 循环tokensfor (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) // 获取tokensreturn 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)
