Mustache模版语法的实现
mustache.js源码解析
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
给定模版字符串和要插入的数据
const templateString = `
<div>
<ol>
{{#students}}
<li>
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ol>
</div>
`;
const templateData = {
students: [
{
'name': '小红',
'hobbies': ['游泳', '健身'],
},
{
'name': '小蓝',
'hobbies': ['喝水', '吃饭'],
},
{
'name': '小绿',
'hobbies': ['游戏', '动漫'],
}
]
}
1. 模版字符串=>tokens
分别定义 头指针pos、尾字符串tail、scanUtil 和 scan 两个方法,其中
- scanUtil:每当扫描到
{{
或}}
时,返回前面的子串,更新 pos和tail - scan:每当扫描到
{{
或}}
时,返回{{
或}}
本身,更新 pos和tail
源码
/**
* 扫描器类
*/
export default class Scanner {
constructor(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* 如果tail为空串,返回true
*/
eos() {
return this.tail === '';
}
/**
* 扫描tail字符串,返回匹配到regular的子串
* 如果匹配不到,则返回空串
*/
scan(regular) {
let match = this.tail.match(regular);
// 匹配不到或匹配到的起始位置不是tail的第一位
if (!match || match.index !== 0) {
return '';
}
let string = match[0];
// 更新tail和头指针位置
this.tail = this.tail.substring(string.length);
this.pos += string.length;
return string;
}
/**
* 扫描tail字符串,返回匹配到regular时前面的子串
* 如果匹配不到,则返回剩余的整个tail字符串
*/
scanUtil(regular) {
let index = this.tail.search(regular);
let match;
switch(index) {
// 匹配不到,返回所有
case -1:
match = this.tail;
this.tail = '';
break;
// 第一位就匹配到,则前面的子串为空串
case 0:
match = '';
break;
// 正常匹配
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
//更新头指针位置
this.pos += match.length;
return match;
}
}
编写解析函数,测试转换
export default function praseTemplate(templateString) {
let tokens = [];
let word;
let scanner = new Scanner(templateString);
while (!scanner.eos()) {
word = scanner.scanUtil("{{");
tokens.push(['text', word]);
scanner.scan("{{");
if (!scanner.eos()) {
word = scanner.scanUtil("}}");
if (word[0] === '#' || word[0] === '/') {
tokens.push([word[0], word.substring(1)])
} else {
tokens.push(['name', word]);
}
scanner.scan("}}");
}
}
return tokens;
}
测试结果
编写格式化函数,将tokens格式化
export default function nestTokens(tokens) {
let nestedTokens = []; // 结果数组,即格式化后的tokens
let sections = []; // 栈
let collector = nestedTokens; // 定义一个收集器指针指向结果数组
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch(token[0]) {
case '#' :
collector.push(token);
sections.push(token); // 遇到#号,token入栈
collector = token[2] = []; // 改变指针指向下一层嵌套数组头部
break;
case '/' :
let section = sections.pop(); // 遇到/号,出栈
collector = sections.length > 0
? sections[sections.length - 1][2] // 当前栈不为空,则拿出栈数组最后一项
: nestedTokens; // 当前栈为空,说明已经回归到数组最外层
break;
default :
collector.push(token); // 初始引用时,相当于给nestedTokens中添加token
}
}
return nestedTokens;
}
测试结果
2. tokens => dom
编写一个lookup函数
/**
* 解析一个如“a.b.c”格式的name字符串,
* 从data对象中取出对应属性的值。
* @param {Object} data
* @param {String} name
*/
export default function lookup(data, name) {
if (name.indexOf('.') > 0) { //排除没有点和只有一个点的情况
let names = name.split('.');
let temp = data;
for (let i = 0; i < names.length; i++) {
temp = temp[names[i]];
}
return temp;
}
return data[name];
}
编写一个renderTokens函数
import lookup from './lookup'
/**
* 使用递归,将tokens转换为dom字符串形式
* @param {Object} tokens
* @param {Object} data
*/
export default function renderTokens(tokens, data) {
let resultStr = '';
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token[0] == 'text') {
resultStr += token[1];
} else if (token[0] == 'name') {
resultStr += lookup(data, token[1]);
} else if (token[0] == '#') {
let v = lookup(data, token[1]);
for (let j = 0; j < v.length; j++) {
resultStr += renderTokens(token[2], {
'.': v[j],
...v[j]
});
}
}
}
console.log(resultStr);
return resultStr;
}
测试结果
3. dom => html
window.MustacheTemplateEngine = {
render(templateString, templateData) {
let tokens = parseTemplate(templateString);
let nestedTokens = nestTokens(tokens);
let domStr = renderTokens(nestedTokens, templateData);
return domStr;
}
}
创建并操作dom节点,将最终得到的dom字符串赋值给节点,渲染到面页
<body>
<div id="container"></div>
<script src="/dist/bundle.js"></script>
<script>
const domStr = MustacheTemplateEngine.render(templateString, templateData);
let container = document.getElementById('container')
container.innerHTML = domStr;
</script>
</body>
测试结果