模板引擎是一个帮助我们在一个有规则的字符串内渲染数据的工具。
举一个例子:
var str = `我叫{{name}},来自于{{school}}
`
var name = '王五'
var school = '一个神秘的大学'
现在有一个str字符串,里面带有花括号的子串需要被渲染成对应的数据,得到这样的字符串,渲染到页面中,就形成了模板引擎。
我叫王五,来自于一个神秘的大学
当然mustache有更复杂的语法,比如:
var templateStr = `
{{#students}}
-
学生{{name}}的爱好是:
{{#hobbies}}
- {{.}}
{{/hobbies}}
{{/students}}
`
var data = {
students:[
{'name':'小红','hobbies':['游泳','健身']},
{'name':'小强','hobbies':['足球','篮球','睡觉']},
{'name':'小明','hobbies':['吃饭','羽毛球']},
]
}
mustache进行模板渲染的过程大致是这样的。
现将模板字符串变为tokens数组,再将tokens数组结合对应数据形成dom字符串。
那么tokens长什么样子呢
var str = `
{{#arr}}
- {{.}}
{{/arr}}
`
//str对应的tokens就长这样
[
['text',''], //token
['#','arr',[ //token
['text','- '
], //token
['name','.'], //token
['text',''] //token
]],
['text','
'] //token
]
最外层数组中的每一个数组元素,都是一个token。
tokens数组将字符串分割成一个个小数组。
当形成规整的tokens后,我们发现text内容可以直接拼接到结果字符串中。而当遇到name时,只需找到对应的数据然后拼接到字符串中即可。至于存在‘#’的时候,是需要特殊处理的。
设mustache对象,里面包含一个render方法。
render里就会有将模板字符串变为tokens数组再变为dom字符串的过程。
var mustache = {
render(templateStr,data) {
//将templateStr变为tokens数组
var tokens = toToken(templateStr);
//将tokens数组变为dom字符串
var domStr = renderTemplate(tokens,data);
return domStr;
}
}
/*
将模板字符串变为tokens数组
*/
export default function toToken(templateStr) {
var tokens = [];
//创建扫描器
var scanner = new Scanner(templateStr)
var words; //用于收集文字
//循环扫描模板字符串
while(!scanner.eos()) {
//收集开始标记前的文字
words = scanner.scanUntil('{{');
if(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 {
tokens.push(['name',words]);
}
}
scanner.scan('}}');
}
//返回折叠后的tokens
return nestTokens(tokens)
}
在tokens过程中, 用到了scanner扫描器来遍历templateStr,从而将templateStr切割为上述所说的几部分。
/*
扫描器类
*/
export default class Scanner {
constructor(templateStr) {
console.log(templateStr);
this.templateStr = templateStr;
//指针
this.pos = 0;
//尾巴,一开始就是模板字符串的原文
this.tail = templateStr;
}
//功能弱,就是走过指定内容,没有返回值
scan(stopTag) {
if(this.tail.indexOf(stopTag) == 0) {
//stopTag有多长,就让指针后移几位,相当于新的tail起点
this.pos += stopTag.length;
this.tail = this.templateStr.substr(this.pos);
}
}
//让指针进行扫描,直到遇到指定内容结束,并且能够返回结束之前路过的文字
scanUntil(stopTag) {
//记录一下执行本方法的时候pos的值
const pos_start = 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_start,this.pos)
}
//判断指针是否已经到头,返回布尔值
eos() {
return this.pos >= this.templateStr.length
}
}
我们看到,在token方法里最后返回的是nestTokens(tokens)。
因为在tokens方法中,只能将字符串处理成扁平的一层,对于带’#’的没有做到嵌套。所以nestTokens的作用就是将扁平的tokens数组变为带有嵌套的tokens数组。
/*
功能是折叠tokens,将#和/之间的tokens能够整合起来,作为他的下标为2的项
*/
export default function nestTokens(tokens) {
//结果数组
var nestedTokens = [];
//栈结构,存放当前操作的token小数组
var sections = [];
//收集器,天生指向nestedTokens结果数组,实则是#元素需要添加第三项内容的指向
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] = [];
console.log(token[1] + '入栈了');
break;
case '/':
//出栈
let section_pop = sections.pop();
//改变收集器为栈结构对尾
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens
}
在上述过程中,我们将模板字符串templateStr变为了带有嵌套的tokens数组,接下来要做的就是,将tokens数组转换为dom字符串。
/*
函数的功能是让tokens数组变为dom字符串
*/
import lookup from './lookup'
import parseArray from './parseArray'
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里存的是对象是不是用点符号调用,如:student.name
resultStr += lookup(data,token[1]);
} else if(token[0] == '#') {
resultStr += parseArray(token,data);
}
}
return resultStr;
}
我们可以知道,在渲染token是以name开头时,我们需要渲染得到是后面的键值,然而有时候{{}}里面的内容是带’.'的,比如{{data.a.name}},这时我们就无法直接通过data[a.name]来获取。
因此lookup函数就是为了解决这一问题而存在。
/*
功能是可以在data对象中,寻找用连续点符号的keyName属性
比如,data = {
a: {
b: {
c: 100
}
}
}
那么lookup(a.b.c)的结果就是100
*/
export default function lookup(data, keyName) {
//如果有点符号
if(keyName.indexOf('.') != -1 && keyName != '.') {
var keys = keyName.split('.');
var temp = data;
for(let i = 0; i < keys.length; i++) {
temp = temp[keys[i]];
}
return temp;
}
//如果没有点符号
return data[keyName];
}
在处理多层嵌套的token时,我们就需要特殊处理。
其核心就是,parseArray与renderTemplate相互调用。
当遇见嵌套的token,我们可以单独用renderTemplate来处理包含在里面的token,不断叠加。
/*
处理数组,结合renderTemplate实现递归
注意这个函数收的参数是token,而不是tokens
token是一个而简单的['#','students',[]]
*/
import lookup from './lookup'
import renderTemplate from './renderTemplate'
export default function parseArray(token,data) {
//得到整体数据data中这个数组要使用的部分
var v = lookup(data,token[1]);
var resultStr = '';
//他要遍历数据,数据有几条,就要有几个这种dom
for(let i = 0; i < v.length; i++) {
resultStr += renderTemplate(token[2],{
...v[i],
'.':v[i]
});
}
return resultStr;
}
这就是完整的模板字符串被渲染的过程了,如有问题多指教。