源码链接:mustache模板引擎
1.模版引擎的介绍
1.1模版引擎是什么?
模版引擎是将数据data变为视图view(html)的解决方案。
1.2历史上将数据变为视图的方法
- 纯DOM法
- 数组join法:本质上是字符串的方法
- ES6的反引号法
- 模板引擎
2.mustache的基本使用
使用方法就是先定义数据,再定义模板。将数据放进一个模板李再赋值给一个变量,通过innHTML写进DOM中
mustache的模版语法
#arr表示开始循环arr数组,/arr表示arr数组循环结束
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
</div>
</li>
{{/arr}}
</ul>
2.1最简单的情况——不循环对象数组
<div id="container"></div>
<script>
// 模板字符串,循环本身{{.}}
var templateStr = `
<h1>我买了一个{{thing}},好{{mood}}</h1>
`;
let data = {
thing: "华为手机",
mood: "开心"
};
// 模板字符串,数据
let domStr = mustache.render(templateStr, data);
let container = document.getElementById("container");
container.innerHTML = domStr;
2.2循环最简单的数组
vue的模板语法中,可以使用v-for指令基于一个数组来渲染一个列表。在mustache中可用{{#arr}}{{/arr}}的形式来循环data中名为arr的数组
<div id="container"></div>
<script>
// 模板字符串,循环本身{{.}}
var templateStr = `
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
`;
let data = {
arr: ['苹果', '鸭梨', '西瓜']
};
// 模板字符串,数据
let domStr = mustache.render(templateStr, data);
let container = document.getElementById("container");
container.innerHTML = domStr;
2.3循环对象数组
补充:script模板写法
在script标签中写入模板,只要type的值不是text/javascript,都不会被当作js执行解析,这样可以在script标签中写入模板,可以高亮自动填充
// 存储模板字符串
<script type="text/template" id="mytemplate">
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
</div>
</li>
{{/arr}}
</ul>
</script>
<script src="jslib/mustache.js"></script>
<div id="container"></div>
<script>
// 模板字符串
var templateStr = document.getElementById("mytemplate").innerHTML;
let data = {
arr: [
{ "name": "小明", "age": 12},
{ "name": "小红", "age": 14}
]
};
// 模板字符串,数据
let domStr = mustache.render(templateStr, data);
let container = document.getElementById("container");
container.innerHTML = domStr;
2.4 循环嵌套【对象数组和简单数组】
<div id="container"></div>
<script>
// 模板字符串,循环数据项{{.}}
var templateStr = `
<ul>
{{#arr}}
<li>
{{name}}的爱好是:
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/arr}}
</ul>
`;
let data = {
arr: [
{"name": "小明", "age": 12, "hobbies": ["游泳", "长跑"]},
]
};
// 模板字符串,数据
let domStr = mustache.render(templateStr, data);
let container = document.getElementById("container");
container.innerHTML = domStr;
2.5 布尔值【控制元素的显示与隐藏】
在mustache中,可以进行布尔值的操作,类似于Vue中的v-show指令,可以控制元素的显示与隐藏。
<div id="container"></div>
<script>
// 模板字符串,循环数据项{{.}}
// {{}}中间不能写表达式
var templateStr = `
{{#m}}
<h1>你好</h1>
{{/m}}
`;
let data = {
m: true
};
// 模板字符串,数据
let domStr = mustache.render(templateStr, data);
let container = document.getElementById("container");
container.innerHTML = domStr;
3.mustache的原理
3.1 实现最简单的模板数据填充
流程:通过正则表达式中的replace()方法来识别双大括号,捕获双大括号内的内容,然后将data对象中对应的数值进行替换。
replace()方法
这个方法接收两个参数,第一个参数可以是一个RegExp对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。第二个参数可以是一个函数。在只有一个匹配项时,函数中的参数:第一个为匹配的部分;第二个捕获的内容,一般为$1表示第一个捕获到的元素;第三个为为匹配的东西的位置,第四个为原串
正则表达式
/{{(\w+}}}/: 用于捕获{{}}双大括号
var templateStr = '<h1>我{{thing}}是{{mood}}的</h1>';
let data = {
thing: "心情",
mood: "happy"
};
// 写一个函数识别{{}},机理是replace方法-可以进行捕获功能:捕获里面的文字(\w+)
// 单纯的replace只能替换第一个
// 如果需要把所有的进行替换,则需要使用正则/g/:全局寻找 w字母
// console.log(templateStr.replace(/{{(\w+)}}/g, function(findStr, $1) {
// // $1为变量,所以不能使用data.$1
// return data[$1];
// }));
// 封装成函数
function render(templateStr, data) {
return templateStr.replace(/{{(\w+)}}/g, function (findStr, $1) {
return data[$1];
});
}
var result = render(templateStr, data);
console.log(result);
但正则表达式的方法难以去实现复杂的情况,比如数组嵌套遍历,所以不能通过正则表达式以及
replace()方法实现mustache。
3.2 mustache实现原理
将模板字符串先编译为tokens,tokens在解析的过程中与数据data结合生成dom字符串
3.2.1 什么是tokens?
tokens是js嵌套数组,就是模板字符串的js表示,它是抽象语法树、虚拟节点的开山鼻祖。
通过识别双大括号{{}},将模板和数据分隔开;而在处理嵌套的情况下,会进行多重循环
3.3实现mustache库的重点
- 将模板字符串编译为tokens形式
- 将tokens结合数据data,解析为DOM字符串
4.手写实现mustache库
将模板字符串编译为tokens形式 ↓
4.1实现Scanner扫描类
- 首先将模板字符串转换为tokens,因此我们需要一个Scanner扫描类,通过指针识别模板字符串来获取我们需要的数据
- scanUtil方法用于识别{{}}找除了【双括号(单纯为{{}}符号)】外的内容。指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
- scan方法用于当scanUtil到达指定的内容{{ 或 }},则将指针往后移动响应的长度
- eos方法用于判断指针是否到头
/* 扫描器类 */
export default class Scanner{
constructor(templateStr) {
// 将模版字符串写到实例身上,由于scanUtil需要使用
this.templateStr = templateStr;
// 指针
this.pos = 0;
// 尾巴,一开始就是模板字符串原文
this.tail = this.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;
// 当尾巴的开头不是stopTag的时候,就说明还没有扫描到stopTag
// 不加且后面容易死循环,写&&很有必要,因为防止找不到,那么寻找到最后也要停止下来
while(!this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++;
// 尾巴包括找到stopTag后面的所有
// 改变尾巴为从当前指针这个字符开始,到最后的全部字符
this.tail = this.templateStr.substring(this.pos);
}
// 开始的位置到指针的位置不包括this.pos
return this.templateStr.substring(pos_backup, this.pos);
}
// 指针是否已经到头,返回布尔值,end od string
eos() {
return this.pos >= this.templateStr.length;
}
}
4.2实现parseTemplateToTokens函数
此函数的功能:将模板字符串变为tokens数组
此函数参数为模板字符串,
- 先创建出扫描器,然后进行循环判断(结束条件为指针到头)
- 首先收集{{前的内容【words = scanner.scanUtil('{{');】,并将其存入tokens中【tokens.push(['text', _words]);】—扩展:在存入tokens之前,需要判断是否有空格(普通空格/标签内的空格)
- 由于{{占字符,指针需要进行移动【scanner.scan('{{');】
- 离开{{,则代表开始进行收集双括号内的内容,直到遇到}}才结束收集【words = scanner.scanUtil('}}');】;双括号中的内容代表的内容与数据data相关,因此判断上有所不同。需要根据截取文字中的首字符去判断推入到tokens的内容。一共分为三种情况:①当遇到‘#’,则代表数组循环的开始【tokens.push(['#', words.substring(1)]);】②当遇到‘/ ’,则代表数组循环的结束【tokens.push(['/', words.substring(1)]);】③除了遇到‘#’和‘/ ’的元素,则将其截取字符设置为name将其存入tokens中【tokens.push(['name', words]);】
- 由于}}占字符,指针需要进行移动【scanner.scan('}}');】
- 将最后获取的tokens放入折叠函数nestTokens,将嵌套的数组进行折叠
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';
// 向外默认暴露这个函数
// 将模板字符串变为tokens数组
export default function parseTemplateToTokens(templateStr) {
var tokens = [];
// 创建扫描器
var scanner = new Scanner(templateStr);
var words;
// 让扫描器工作
while (!scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUtil('{{');
if(words != '') {
// 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格
// 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格
// 是不是在尖角号里面,默认为不是
let isJJH = false;
let _words = '';
for(let i = 0; i < words.length; i++) {
// 判断是否在标签里
if(words[i] == '<') {
isJJH = true;
} else if(words[i] == '>') {
isJJH = false;
}
// 如果这项不是空格,拼接上
if(!/\s/.test(words[i])) {
_words += words[i];
} else {
// 如果这项是空格,只有当它在标签内的时候,才拼接上
if(isJJH) {
_words += ' ';
}
}
}
tokens.push(['text', _words]);
// 过双大括号
scanner.scan('{{');
// 收集开始标记出现之前的文字
words = scanner.scanUtil('}}');
if(words != '') {
// 这个words就是{{}}中间的东西,判断一下首字符
if(words[0] == "#") {
// 存起来,从下标为1的项开始存,因为下标为0的项是#
tokens.push(['#', words.substring(1)]);
}else if(words[0] == "/") {
tokens.push(['/', words.substring(1)]);
} else {
tokens.push(['name', words]);
}
}
scanner.scan('}}');
}
}
// 返回折叠的tokens
return nestTokens(tokens);
}
4.3实现nestTokens函数
此函数的功能:折叠tokens,将#和/之间的tokens能够整合起来,作为下标为3的项。在nestTokens()中使用了栈的思路来折叠tokens,还巧妙的运用了collector收集器,让collector数组在不同的时候指向不同的数组。
- 在开始前收集器collector指向结果数组nestTokens,当遇到’#‘的时候,将collector指向token[2],让后面的数组直接push进token下标为2的数组中,用于实现嵌套;当遇到’/‘的时候,先弹出栈顶,根据selections的长度来判断,是改变收集器为栈顶那项的下标为2的数组【意味还有数组嵌套】还是直接返回结果数组
- 最后返回结果数组
// 思路:栈
export default function nestTokens(tokens) {
// 结果数组
let nestTokens = [];
// 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
let sections = [];
// 收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组
// 收集器的指向会变化。当遇见#的时候,收集器会指向这个token的下标为2的新数组
let collector = nestTokens;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch(token[0]) {
case '#':
// 收集器中放入这个token
collector.push(token);
// 入栈
sections.push(token);
// 收集器要换人,给token添加下标为2的项,并且将收集器指向它
collector = token[2] = [];
break;
case '/':
// 出栈,pop()会返回刚刚弹出的顶
sections.pop();
// 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
// 如果collector没有值则指回结果
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens;
break;
default:
// 甭管当前的collector是谁,可能是结果nestedTokens,也可以能是token的下标为2的数组,甭管是谁,推入collector即可
collector.push(token);
}
}
return nestTokens;
}
将tokens结合数据data,解析为DOM字符串 ↓
4.4实现lookup函数
此函数的功能是用于获取有层叠的数据【可以在dataObj对象中,寻找用连续点符号的keyName属性】
- 首先判断keyName中是否有点符号并且不是单纯的点元素
- 如果属性中有点符号,则使用split进行拆分,通过循环找出需要data值
/*
{
a: {
b: {
c: 100
}
}
}
那么lookup(dataObj, 'a.b.c)结果就是100
*/
export default function lookup(dataObj, keyName) {
// 看看keyName中有没有点符号,但不能是.本身,{{.}}单纯.也用于解析数据
if(keyName.indexOf('.') != -1 && keyName != '.') {
// 如果有点符号,那么拆开
let keys = keyName.split('.');
// 设置一个临时变量,这个临时变量用于周转,一层一层找下去
let temp = dataObj;
// 每找一层,就把他设置为新的临时变量
for(let i = 0; i <keys.length; i++) {
// 拆分属性来一层一层寻找需要的data
temp = temp[keys[i]];
}
return temp;
}
// 如果这里面没有点符号
return dataObj[keyName];
}
4.5实现parseArray函数
此函数的作用: 处理数组,结合renderTemplate实现递归。
此函数的使用还需要使用lookup【得到整体数据data中这个数组要使用的部分】、renderTemplate【有嵌套数据需要递归调用】
import lookup from "./lookup";
import renderTemplate from "./renderTemplate";
/*
* 注意:这个函数的参数是token,而不是tokens
* token是什么,就是一个简单的['#', 'students',[]]
* 这个函数要递归调用renderTemplate函数,调用的次数由data决定
* {
students: [
{'name': '小明'},
{'name': 'John'},
{'name': 'Tom'}
]
};
那么parseArray()函数要递归调用renderTemplate函数3次,因为数组的长度为3
*/
export default function parseArray(token, data) {
// console.log(token, data);
// 得到整体数据data中这个数组要使用的部分
let v = lookup(data, token[1]);
// 结果字符串
let resultStr = '';
console.log(v);
// 遍历v数组,v一定是数组
// 下面这个循环可能是整个包中最难思考的一个循环
// 它是遍历数据,而不是遍历tokens,数组中的数据有几条,就要遍历几条
for (let i = 0; i < v.length; i++) {
// 拼接返回,递归调用
// 递归调用renderTemplate
resultStr += renderTemplate(token[2], {
// 现在这个数据小对象,是v[i]的展开,就是v[i]本身
...v[i],
// 这里要补一个"."属性并且替代当前项
'.': v[i]
});
}
// 返回的结果会加到最后结果字符串中,由于最终测试的地方是数组
// 因此思路要局限在数组的解析上
return resultStr;
}
4.6实现renderTemplate函数
此函数的功能是让tokens数组变为dom字符串
- 循环遍历tokens,首先判断token的首项,根据首项不同去执行不同的流程。
此函数的使用还需要lookup【为了防止属性有层级】、parseArray【递归识别token】
import lookup from './lookup.js';
import parseArray from './parseArray.js';
export default function renderTemplate(tokens, data) {
let resultStr = '';
// 结果字符串
// 遍历tokens
for(let i = 0; i < tokens.length; i++) {
let token = tokens[i];
// 看类型
if(token[0] == 'text') {
resultStr += token[1];
} else if (token[0] == 'name') {
// 如果是name类型,那么就直接使用它的值,当然要用lookup
// 因为防止这里是"a.b.c"有点的形式
resultStr += lookup(data, token[1]);
} else if (token[0] == '#') {
// 递归,解析下一层
// 调用parseArray函数来辅助识别token[0]='#'的token
resultStr += parseArray(token, data);
}
}
return resultStr;
}
4.7实现mustache处理数据
- 通过parseTemplateToTokens实现将模板字符串变为tokens
- 通过renderTemplate实现将tokens变为dom
import parseTemplateToTokens from './parseTemplateToTokens.js';
import renderTemplate from './renderTemplate.js';
// 全局提供templateEngine对象
window.templateEngine = {
render(templateStr, data) {
// 1.把模版字符串变成tokens,调用parseTemplateToTokens函数,让模版字符串能够变为tokens数组
var tokens = parseTemplateToTokens(templateStr);
// 2.把tokens变为dom,调用renderTemplate函数,让tokens数组变为dom字符串
var domStr = renderTemplate(tokens, data);
return domStr;
}
}
来自尚硅谷Vue源码解析之mustache模板引擎的学习笔记