学习地址 : https://www.bilibili.com/video/BV1EV411h79m?vd_source=a81826692f4afea80764f4048dc1ae0a
代码地址 :
-
什么是模板引擎
模板引擎是将数据变为视图的最优解
将数据变为视图有很多种方法:
比如纯DOM法:
<ul id="list">
</ul>
<script>
var arr = [
{ "name": "小明", "age": 12, "sex": "男" },
{ "name": "小红", "age": 11, "sex": "女" },
{ "name": "小强", "age": 13, "sex": "男" }
];
var list = document.getElementById('list');
for (var i = 0; i < arr.length; i++) {
// 每遍历一项,都要用DOM方法去创建li标签
let oLi = document.createElement('li');
// 创建hd这个div
let hdDiv = document.createElement('div');
hdDiv.className = 'hd';
hdDiv.innerText = arr[i].name + '的基本信息';
// 创建bd这个div
let bdDiv = document.createElement('div');
bdDiv.className = 'bd';
// 创建三个p
let p1 = document.createElement('p');
p1.innerText = '姓名:' + arr[i].name;
bdDiv.appendChild(p1);
let p2 = document.createElement('p');
p2.innerText = '年龄:' + arr[i].age;
bdDiv.appendChild(p2);
let p3 = document.createElement('p');
p3.innerText = '性别:' + arr[i].sex;
bdDiv.appendChild(p3);
// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
oLi.appendChild(hdDiv);
oLi.appendChild(bdDiv);
list.appendChild(oLi);
}
</script>
数组的JOIN方法:
<script>
var arr = [
{ "name": "小明", "age": 12, "sex": "男" },
{ "name": "小红", "age": 11, "sex": "女" },
{ "name": "小强", "age": 13, "sex": "男" }
];
var list = document.getElementById('list');
// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for (let i = 0; i < arr.length; i++) {
list.innerHTML += [
'<li>',
' <div class="hd">' + arr[i].name + '的信息</div>',
' <div class="bd">',
' <p>姓名:' + arr[i].name + '</p>',
' <p>年龄:' + arr[i].age + '</p>',
' <p>性别:' + arr[i].sex + '</p>',
' </div>',
'</li>'
].join('')
}
</script>
ES6的反引号 :
<script>
var arr = [
{ "name": "小明", "age": 12, "sex": "男" },
{ "name": "小红", "age": 11, "sex": "女" },
{ "name": "小强", "age": 13, "sex": "男" }
];
var list = document.getElementById('list');
// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for (let i = 0; i < arr.length; i++) {
list.innerHTML += `
<li>
<div class="hd">${arr[i].name}的基本信息</div>
<div class="bd">
<p>姓名:${arr[i].name}</p>
<p>性别:${arr[i].sex}</p>
<p>年龄:${arr[i].age}</p>
</div>
</li>
`;
}
</script>
-
mustache模板引擎
mustache是最早的模板引擎,比Vue诞生要早得多,它的底层机理在当时是非常具有创造性,轰动性的,为后续的模板引擎发展提供了崭新的思路
mustache是'胡子'的意思,它的嵌入标记{{ }}非常像胡子,也可以叫它大胡子语法,{{ }}语法也被Vue沿用
-
mustache处理过程
tokens就是一个js的嵌套数组,也就是模板字符串JS的表现形式
首先,我们需要先将模板字符串编译为tokens的形式:
然后,我们需要将自己定义的数据与tokens结合,变为dom:
当模板字符串有嵌套的情况时,它将被编译为嵌套的tokens:
-
手写简化版mustache库
目录结构 :
首先,我们需要先将模板字符串变为tokens :
将模板字符串变为tokens需要几个步骤:
第一创建模板字符串,并且调用render函数 :
// 模板字符串
var templateStr = `
<div>
<ul>
{{#students}}
<li>
学生{{name}}的爱好是
<ul>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ul>
</li>
{{/students}}
</ul>
</div>
`;
// 数据
var data = {
students: [
{ 'name': '小明', 'hobbies': ['游泳', '健身'] },
{ 'name': '小红', 'hobbies': ['足球', '篮球', '羽毛球'] },
{ 'name': '小强', 'hobbies': ['吃饭', '睡觉'] }
]
}
// 调用render
var domStr = awei_TemplateEngline.render(templateStr, data)
render函数接受了两个参数,一个是模板字符串,还有一个是数据 :
// 全局提供awei_TemplateEngline对象
window.awei_TemplateEngline = {
// 渲染方法
render(templateStr, data) {
console.log('render函数被调用,我们需要让Scanner工作');
// 实例化一个扫描器,构造式提供一个模板字符串参数
// 这个扫描器针对模板字符串工作
var scanner = new Scanner(templateStr)
}
}
现在,我们要想一下如何才能识别模板字符串中的{{ }},在mustache库中,用到了一个扫描器,这个扫描器是一个Scanner类 ,那么他是如何工作的呢?
他用到了两个指针,当第二个指针遇到{{时,scanUntil需要收集在{{之前的文字,并且通过scan路过{{
代码实现 :
// 扫描器类
export default class Scanner {
// 通过 new 命令创建对象实例时,自动调用constructor方法
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.substr(this.pos)
}
}
// 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前的路过的文字
scanUntil(stopTag) {
// 记录一下,执行本方法的时候pos的值
const pos_backup = this.pos
// 当尾巴的开头不是stopTag的时候,就说明还没有扫描到stopTag
while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++
// 改变尾巴为从当前指针这个字符开始,到最后的全部字符
this.tail = this.templateStr.substr(this.pos)
}
return this.templateStr.substring(pos_backup, this.pos)
}
// 指针是否已经到头,返回布尔值
eos() {
return this.pos >= this.templateStr.length
}
}
现在,我们已经完成了对模板字符串的扫描,接下来,我们可以在创建一个模块,将模块命名为parseTemplateToTokens,他的功能就是用来将模板字符串编译为tokens,并且我们将刚才在render函数中调用的Scanner放到parseTemplateToTokens中 :
render函数:
// 全局提供awei_TemplateEngline对象
window.awei_TemplateEngline = {
render(templateStr, data) {
// 调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
var tokens = parseTemplateToTokens(templateStr)
}
}
parseTemplateToTokens函数 :
import Scanner from './Scanner'
/**
* 将模板字符串变为tokens数组
*/
export default function parseTemplateToTokens(templateStr) {
var tokens = []
// 创建扫描器
var scanner = new Scanner(templateStr)
var words
while (!scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUntil('{{')
if (words != '') {
// 存起来,去掉空格
tokens.push(['text', words.replace(/\s{1,}(<)|(>)\s{1,}/g, '$1$2')])
}
// 过双大括号
words = scanner.scan('{{')
// 收集开始标记出现之前的文字
words = scanner.scanUntil('}}')
if (words != '') {
// 这个words就是{{}}中间的东西,判断一下首字符
if (words[0] == '#') {
// 从下标为一得项开始存,因为下标为0的项是#
tokens.push(['#', words.substring(1)])
} else if (words[0] == '/') {
// 从下标为一得项开始存,因为下标为0的项是/
tokens.push(['/', words.substring(1)])
} else {
// 存起来
tokens.push(['name', words])
}
}
// 过双大括号
words = scanner.scan('}}')
}
}
现在,我们写的parseTemplateToTokens其实是有问题的,他并不能实现嵌套tokens,那么接下来我们就需要将零散的tokens嵌套起来,在写这个方法之前,我们需要再看一下别的知识
-
数据结构--栈
栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
所以,我们折叠tokens就可以用到栈的思想
在我们的代码中,我们需要遇见#号进栈,遇到/出栈
下面,我们创建折叠tokens的模块 nestedTokens :
/**
* 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为他的下标为3的项
*/
export default function nextTokens(tokens) {
// 结果数组
var nestedTokens = []
// 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
var sections = []
// 收集器,一开始指向结果数组,引用类型值,所以指向的是同一个数组
var collector = nestedTokens
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 = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens
break
default:
collector.push(token)
}
}
return nestedTokens
}
在这个算法中,有一个很巧妙的引用类型值的运用,我们对collector的添加操作其实并不是将数据添加到 collector里面,而是添加到collector一开始指向的结果数组,collector只是起到了一个收集器的作用,收集器的指向也会发生改变,当遇见#号的时候,收集器就会指向这个token的下标为2的新数组
我们在parseTemplateToTokens引入nestedTokens :
import Scanner from './Scanner'
import nextTokens from './nestTokens' // 修改
/**
* 将模板字符串变为tokens数组
*/
export default function parseTemplateToTokens(templateStr) {
var tokens = []
// 创建扫描器
var scanner = new Scanner(templateStr)
var words
while (!scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUntil('{{')
if (words != '') {
// 存起来,去掉空格
tokens.push(['text', words.replace(/\s{1,}(<)|(>)\s{1,}/g, '$1$2')])
}
// 过双大括号
words = scanner.scan('{{')
// 收集开始标记出现之前的文字
words = scanner.scanUntil('}}')
if (words != '') {
// 这个words就是{{}}中间的东西,判断一下首字符
if (words[0] == '#') {
// 从下标为一得项开始存,因为下标为0的项是#
tokens.push(['#', words.substring(1)])
} else if (words[0] == '/') {
// 从下标为一得项开始存,因为下标为0的项是/
tokens.push(['/', words.substring(1)])
} else {
// 存起来
tokens.push(['name', words])
}
}
// 过双大括号
words = scanner.scan('}}')
}
// 返回折叠收集的tokens
return nextTokens(tokens) // 修改
}
现在,我们已经将模板字符串转换为了tokens,下面我们就需要使用tokens结合数据,生成dom字符串了
使用tokens结合数据,生成dom字符串
我们先创建一个renderTemplate模块,这个模块的功能就是让tokens数组变为dom字符串
import lookup from './lookup'
import parseArray from './parseArray'
/**
* 函数的功能是让tokens数组变为dom字符串
*/
export default function renderTemplate(tokens, data) {
// 结果字符串
var 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] == '#') {
resultStr += parseArray(token, data)
}
}
return resultStr
}
可以看到,我们在renderTemplate中引入了两个模块,lookup 和 parseArray
lookup的功能是为了防止数据中出现a.b.c的形式的,在{{ }}语法中,我们可以写成{{students.name}}的形式,但是在JS中是不认识.形式的,所以我们需要lookup来帮我们处理 :
/**
* 功能是可以在dataObj对象中,寻找用连续点符号的keyName属性
* 比如,dataObj是
* {
* a:{
* b:{
* c:100
* }
* }
* }
* 那么lookup(dataObj,'a.b.c')结果就是100
*/
export default function lookup(dataObj, keyName) {
// 看看keyName中有没有.符号,但不能是.本身
if (keyName.indexOf('.') != -1 && keyName != '.') {
// 如果有.符号,那么拆开
var keys = keyName.split('.')
// 设置一个临时变量,用于周转,一层一层找下去
var temp = dataObj
// 每找一层,就把他设置为新的临时变量
for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]]
}
return temp
}
// 如果没有.符号
return dataObj[keyName]
}
parseArray的功能是 : 当我们遇见#号的时候,token下标为2的那一项又会是一个数组,所以我们需要用到递归的思路来解决这个问题,他接受两个参数,一个是当前#的这一项token,第二个是数据 :
import lookup from './lookup'
import renderTemplate from './renderTemplate'
/**
* 处理数组,结合renderTemplate实现递归
* 注意:这个函数收的是一个token
* 递归的次数由data决定
*
*/
export default function parseArray(token, data) {
// 得到整体数据data中这个数组要使用的部分
var v = lookup(data, token[1])
// 结果字符串
var resultStr = ''
// 遍历v数组
for (let i = 0; i < v.length; i++) {
resultStr += renderTemplate(token[2], {
// 是v[i]的展开
...v[i],
// 补充一个.属性
'.': v[i]
})
}
return resultStr
}
接下来我们就可以在render函数中调用renderTemplate,将tokens数组变为dom字符串 :
import parseTemplateToTokens from './parseTemplateToTokens'
import renderTemplate from './renderTemplate'
//全局提供awei_TemplateEngline对象
window.awei_TemplateEngline = {
// 渲染方法
render(templateStr, data) {
// 调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
var tokens = parseTemplateToTokens(templateStr)
// 调用renderTemplate函数,让tokens数组变为dom字符串
var domStr = renderTemplate(tokens, data)
return domStr
}
}
最后,我们在index.html中调用render,渲染上树 :
<body>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
<script>
// 模板字符串
var templateStr = `
<div>
<ul>
{{#students}}
<li>
学生{{name}}的爱好是
<ul>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ul>
</li>
{{/students}}
</ul>
</div>
`;
// 数据
var data = {
students: [
{ 'name': '小明', 'hobbies': ['游泳', '健身'] },
{ 'name': '小红', 'hobbies': ['足球', '篮球', '羽毛球'] },
{ 'name': '小强', 'hobbies': ['吃饭', '睡觉'] }
]
}
// 调用render
var domStr = awei_TemplateEngline.render(templateStr, data)
// 渲染上树
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
</body>
-
总结
在mustache的源码中,还有Context类和Writer类,Context类里面有一个缓存机制,Writer类是我们手写的把tokens变成dom,但我们写的是简化版的,所以就不需要顾虑这么多了,我觉得nestedTokens模块中的栈思想的算法确实是很巧妙,也非常值得去学习,这篇文章也是我对于学习mustache的总结,在B站白嫖是真舒服啊!