mustache 模板引擎
模板引擎是将数据变成试图的最优雅的解决方
历史上出现的数据变成视图的方法
- 纯 DOM 方法
- 数组 join 法
- ES6 的反引号法
- 模板引擎
1. 纯 DOM 方法
需要的节点全靠生成后上树, 节点属性, 相当的复杂
<body>
<ul id="list"></ul>
<script>
const arr = [
{ name: "123", age: 123 },
{ name: "456", age: 456 },
{ name: "789", age: 789 },
];
const list = document.getElementById("list");
for (let index = 0; index < arr.length; index++) {
const element = arr[index];
// 需要的节点全靠生成后上树,相当的复杂
let oli = document.createElement("li");
oli.innerText = element.name;
let oli = document.createElement("li");
oli.innerText = element.age;
// 上树
list.appendChild(oli);
}
</script>
</body>
2. 数组的 join 方法
使用了数组的有结构的形式 数组的 join 出来字符串 ['A','B'].join('') ---> 'AB'
const arr = [
{ name: "123", age: 123 },
{ name: "456", age: 456 },
{ name: "789", age: 789 },
];
const list = document.getElementById("list");
for (let index = 0; index < arr.length; index++) {
const element = arr[index];
list.innerHTML += [
"<li>名字: " + element.name + "</li>",
"<li>年龄: " + element.age + "</li>",
].join("");
}
3.ES6 的反引号法
const arr = [
{ name: "123", age: 123 },
{ name: "456", age: 456 },
{ name: "789", age: 789 },
];
const list = document.getElementById("list");
for (let index = 0; index < arr.length; index++) {
const element = arr[index];
list.innerHTML += `
<li>名字:${element.name}</li>
<li>年龄:${element.age}</li>
`;
}
4. 模板引擎
mustache 基本使用
循环
var templateStr = `
<ul>
{{#arr}}
<li>名字:{{name}}</li>
<li>年龄:{{age}}</li>
{{/arr}}
</ul>
`;
var data = {
arr: [
{ name: "123", age: 123 },
{ name: "456", age: 456 },
{ name: "789", age: 789 },
],
};
const domStr = Mustache.render(templateStr, data);
console.log(domStr);
// <ul>
// <li>名字:123</li>
// <li>年龄:123</li>
// <li>名字:456</li>
// <li>年龄:456</li>
// <li>名字:789</li>
// <li>年龄:789</li>
// </ul>
list.innerHTML += domStr;
不循环
var templateStr = `
<li>名字:{{name}}</li>
<li>年龄:{{age}}</li>
`;
var data = { name: "123", age: 123 };
const domStr = Mustache.render(templateStr, data);
list.innerHTML += domStr;
循环简单数组
.就可以表示项本身
var templateStr = `
<ul>
{{#arr}}
<li>名字:{{.}}</li>
{{/arr}}
</ul>
`;
var data = {
arr: [1, 2, 3, 4, 5, 6],
};
const domStr = Mustache.render(templateStr, data);
list.innerHTML += domStr;
数组的嵌套
var templateStr = `
<ul>
{{#arr}}
<li>名字:{{name}}</li>
<li>年龄:{{age}}</li>
<div>
{{#aihao}}
爱好: {{.}}
{{/aihao}}
<div/>
{{/arr}}
</ul>
`;
var data = {
arr: [
{ name: "123", age: 123, aihao: ["吃饭1", "打球1"] },
{ name: "456", age: 456, aihao: ["吃饭2", "打球2"] },
{ name: "789", age: 789, aihao: ["吃饭3", "打球3"] },
],
};
const domStr = Mustache.render(templateStr, data);
// 名字:123
// 年龄:123
// 爱好: 吃饭1 爱好: 打球1
// 名字:456
// 年龄:456
// 爱好: 吃饭2 爱好: 打球2
// 名字:789
// 年龄:789
// 爱好: 吃饭3 爱好: 打球3
list.innerHTML += domStr;
使用布尔值
var templateStr = `
{{#m}}
<li>名字:1</li>
{{/m}}
`;
var data = { m: true }; // true就显示,false就不显示
const domStr = Mustache.render(templateStr, data);
list.innerHTML += domStr;
早期还没有反引号的神操作
<script type="text/template" id="myTemplate">
<ul>
{{#arr}}
<li>名字:{{name}}</li>
<li>年龄:{{age}}</li>
<div>
{{#aihao}}
爱好: {{.}}
{{/aihao}}
<div/>
{{/arr}}
</ul>
</script>
<!-- 神人啊,这都能想得到? -->
<script>
var templateStr = document.getElementById("myTemplate").innerHTML;
var data = {
arr: [
{ name: "123", age: 123, aihao: ["吃饭1", "打球1"] },
{ name: "456", age: 456, aihao: ["吃饭2", "打球2"] },
{ name: "789", age: 789, aihao: ["吃饭3", "打球3"] },
],
};
const domStr = Mustache.render(templateStr, data);
// 名字:123
// 年龄:123
// 爱好: 吃饭1 爱好: 打球1
// 名字:456
// 年龄:456
// 爱好: 吃饭2 爱好: 打球2
// 名字:789
// 年龄:789
// 爱好: 吃饭3 爱好: 打球3
list.innerHTML += domStr;
</script>
mustache 底层核心机理
mustache 不能用简单的正则表达式思路实现,在复杂模板字符串就不支持了
只适合最简单的
var templateStr = "<li>名字:{{name}}</li>";
var data = { name: "2123" };
const newStr = templateStr.replace(/\{\{(\w+)\}\}/g, function (str, key) {
console.log(str, key); // {{name}} name
return data[key];
});
list.innerHTML += newStr;
模板字符串--编译-->tokens--解析(结合数据)-->dom字符串
tokens
就是一个 js 的嵌套数据,也就是模板字符串的 js 表示
他是 抽象语法树 虚拟节点 等等的开山鼻祖
每次的 {{ }} 都是一个分割,都分为二
单层模板字符串转 tokens
<h1>我买了一个 {{ thing }},好{{ mood }}啊</h1>
[
['text':'<h1>我买了一个'], // token
['name':'thing'], // token
['text':'好'], // token
['name':'mood'], // token
['text':'啊</h1>'] // token
]
多层模板字符串转 tokens
<div>
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
</div>
以此类推,更多层时候逐层处理即可
[
["text", "<div><ul>"],
[
"#",
"arr",
[
["text", "<li>"],
["name", "."],
["text", "</li>"],
],
],
["text", "</ul></div>"],
];
源码 tokens
// 这里就是返回的tokens
return nestTokens(squashTokens(tokens));
手写实现 mustache 库
生成库是 UMD,这样意味着他同事可以在 nodejs 环境中使用,也可以在浏览器环境当中使用,实现 UMD 不难,只需要一个"通用头"即可
栈
后进先出,先进后出, 在我们这里,遇到#旧进栈,遇到/旧出栈
[
["name", "\n <div>\n <ol>\n "],
["#", "students"],
["name", "\n <li>\n 学生"],
["text", "item.name"],
["name", "的爱好是\n <ol>\n "],
["#", "item.hobbies"],
["name", "\n <li>{.}</li>\n "],
["/", "item.hobbies"],
["name", "\n </ol>\n </li>\n "],
["/", "students"],
["name", "\n </ol>\n </div>\n "],
];
手写简易源码
index.js
import parseTemplateToTokens from "./parseTemplateToTokens.js";
import renderTemplate from "./renderTemplate.js";
window.LGQ_tem = {
// 渲染方法
render(templateStr, data) {
// 调用 parseTemplateToTokens 来处理模板字符串 位 tokens 数组
var tokens = parseTemplateToTokens(templateStr);
// 调用renderTemplate函数,让tokens数组变成dom字符串
var domStr = renderTemplate(tokens, data);
return domStr;
},
};
parseTemplateToTokens.js
import Scanner from "./Scanner.js";
import nextTokens from "./nextTokens.js";
// 将模板字符串变成tokens、 数组
export default function parseTemplateToTokens(templateStr) {
var tokens = [];
var scanner = new Scanner(templateStr);
var words;
// 当scan没有当头就循环持续工作
while (scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUtil("{{");
// 这个words就是{{}}中间的东西,我们要判断下,如果首字符是#号
if (words != "") {
// 存起来
// 这里需要处理空格,文字之间的空格去除,但是标签上的不能去掉,这里是暴力去掉了所有
// 尝试写一下去掉空格,智能判断是文字之间的空格,还是标签上的不能去掉
let isInJJh = false;
var _words = "";
for (let i = 0; i < words.length; i++) {
const element = words[i];
if (element === "<") {
isInJJh = true;
} else if (element == ">") {
isInJJh = false;
}
if (!/\s/.test(element)) {
_words += element;
} else {
// 如果是空格,只有在标签内才拼接
if (isInJJh) {
// 不在标签内
_words += element;
}
}
}
tokens.push(["text", _words]);
}
// 过双大括号
scanner.scan("{{");
// 收集开始标记出现之前的文字
// 存起来 这个变量需要特殊处理,情况多
words = scanner.scanUtil("}}");
if (words != "") {
if (words[0] == "#") {
// 存起来 从下标为1的项开始存,因为下标0的项是#
tokens.push(["#", words.substring(1)]);
} else if (words[0] == "/") {
// 存起来,从下标为1的项开始存,因为下标0的项是/
tokens.push(["/", words.substring(1)]);
} else {
// 存起来
tokens.push(["name", words]);
}
}
// 过双大括号
scanner.scan("}}");
}
// 返回折叠收集的tokens
return nextTokens(tokens);
}
renderTemplate.js
import loopup from "./lookup.js";
import parseArray from "./parseArray.js";
// 函数的功能是让tokens数组变成dom字符串
export default function renderTemplate(tokens, data) {
// 结果字符串
var resultStr = "";
// 遍历tokens
for (let index = 0; index < tokens.length; index++) {
const token = tokens[index];
// 看类型
if (token[0] === "text") {
resultStr += token[1];
} else if (token[0] === "name") {
// 这样 data[token[1]] 不能处理 {{a.b.c}}的情况, data[a.b.c]js不认识的
// 所以需要自己写个lookup方法来单独处理数据
resultStr += loopup(data, token[1]);
} else if (token[0] == "#") {
resultStr += parseArray(token, data);
}
}
return resultStr;
}
Scanner.js
export default class Scanner {
// 官方对于这两个方法的定义是;
// scanUtil方法是用于 处理 {{ 和 }} 之外匹配的内容
// sacn方法妗妗是处理{{ 和 }} 这两个符号
// "我买了一个 {{ thing }} ,好{{mood}}啊";
// --scanUtil--scan--scanUtil--scan--scanUtil
constructor(templateStr) {
// 保存模板字符到类中
this.templateStr = templateStr;
this.pos = 0; // 指针
// 尾巴.一开始就是模板字符串的全文,来判断当前指针是否走完来用
this.tail = templateStr;
}
// 就是走过指定内容 无返回值
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
// tag有多长,比如{{长度是2,就让我们的指针后移动多少位
this.pos += tag.length;
// 尾巴也要变,改变为从当前到最后
this.tail = this.templateStr.substring(this.pos);
}
}
// 让咱们的指针去扫描,直到遇到指定内容结束,并且返回结束之前扫描的文字
scanUtil(stopTag) {
// 记录一下,开始执行本方法的时候pos的值
const pos_backup = this.pos;
// 向右走字符,也就是pos+,但是也不能一只移动,就需要一个尾巴(可以检查), 不能一直往下走
while (this.tail.indexOf(stopTag) != 0 && this.eos()) {
this.pos++;
// pos移动,那么也需要尾巴跟着动,尾巴包括当前指针这一位的后面的所有
this.tail = this.templateStr.substring(this.pos);
}
// 返回的不能包括当前指针位
return this.templateStr.substring(pos_backup, this.pos);
}
// 指针是否到头,返回布尔值
eos() {
return this.pos < this.templateStr.length;
}
}
nextTokens.js
// 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为他的下标为3的项
export default function nextTokens(tokens) {
// 结果数组
var nestedTokens = [];
// 栈结构,栈顶,(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
var sections = [];
// 收集器(天生指向nestedTokens结果数组,引用数据类型,所以指向的是同一个数组)
var collector = nestedTokens; //收集器的指向会变化,当遇到#号的时候,指向这个下标为2的新数组,
for (let index = 0; index < tokens.length; index++) {
const token = tokens[index];
switch (token[0]) {
case "#":
// 往收集器中放入token
collector.push(token);
// 入栈
sections.push(token);
// 收集器要换人,给收集器指向当前子数组里
collector = token[2] = [];
break;
case "/":
// 出栈 pop会返回刚刚弹出的项
sections.pop();
// 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组, 就是向为目前结构的上一层
// 如果栈里面有东西,那就回第二项,但是没有的话,就回到最顶层了
collector =
sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
// 不要管当前的 collector 是什么,可能是最顶层,也可能是子项,只要往里面推就行
collector.push(token);
break;
}
}
// 这个先进去的#肯定是后面才/出来 (写法很乱很乱)
// for (let index = 0; index < tokens.length; index++) {
// const token = tokens[index];
// switch (token[0]) {
// // 入栈(压栈)
// case "#":
// // 给这个token下标为2的项创建一个数组,收集他们的子元素
// token[2] = [];
// sections.push(token);
// nestedTokens.push(token);
// break;
// // 出栈(弹栈)
// case "/":
// // pop会返回刚才弹出的项
// let sectionPop = sections.pop();
// sections;
// // 刚刚弹出的项还没有加到结果当中
// nestedTokens.push(sectionPop);
// break;
// default:
// // 判断栈这个队列当前情况,如果是空的,我就往结尾放,
// if (sections.length == 0) {
// // 说明这个时候还没有遇到任何#,或者已经出去了
// nestedTokens.push(token);
// } else {
// // 往这个子项的数组里面加
// sections[sections.length - 1][2].push(token);
// }
// }
// }
return nestedTokens;
}
lookup.js
// 函数的功能是可以在 dataObj 对象之中,寻找用连续点符号对应的数据
// 例如
// dataObj = {
// a:{
// b:{
// c:100
// }
// }
// }
// 那么
// loopup(dataObj, 'a.b.c') 返回 100
export default function loopup(dataObj, keyName) {
// 自己写的方法
// 根据.来进行分割,但是还不能是. (因为是.的时候就是循环简单的单层数组数据)
if (keyName.indexOf(".") != -1 && keyName !== ".") {
const arr = keyName.split(".");
// 临时变量用于一层一层往下找
let tem = dataObj;
if (arr.length > 1) {
arr.forEach((element) => {
// 在剩余的tem里面一层一层找
tem = tem[element];
});
console.log("dataObj", dataObj);
return tem;
}
}
return dataObj[keyName];
}
parseArray.js
import loopup from "./lookup.js";
import renderTemplate from "./renderTemplate.js";
/**
* 处理数据,结合 renderTemplate实现递归
* 注意: 这个函数的参数是token, 而不是 tokens;
* token是什么, 就是一个简单的[("#", "student", [])];
*
* 这个函数递归调用 renderTemplate 函数,调用多少次?
* 调用的次数由data决定
* 比如data的形式是这样的
* {
students: [
{ name: "小明", hobbies: ["游泳", "健身"] },
{ name: "小红", hobbies: ["足球", "篮球", "羽毛球"] },
{ name: "小强", hobbies: ["吃饭", "睡觉"] },
],
}
那么我们这个函数 parseArray 要调用 renderTemplate 函数三次,因为数组的长度是3
* @param {*} token
* @param {*} data
*/
export default function parseArray(token, data) {
// 得到data中整体的数据
var v = loopup(data, token[1]);
// 结果字符串
var resultStr = "";
// 遍历v,不一定是数组,因为#后面也可以跟布尔值
// 这里是遍历数据的,数据有几个就需要渲染tokens几次
for (let index = 0; index < v.length; index++) {
// 这个需要补一个.属性的识别
resultStr += renderTemplate(token[2], {
// 补充一个.属性
".": v[index],
// 相当于这个数据的小对象,是v[index]的展开,就是v[index]本身
...v[index],
});
}
return resultStr;
}