Vue 源码 – mustache 模版引擎
1. 什么是模版引擎
数据 --> 视图
- 纯 DOM 法
- 数组 join 法
- ES6 反引号
- 模版引擎
模版引擎是将数据变为视图最优雅的解决方案
[
{"name":"张三", "age":18},
{"name":"李四", "age":20},
{"name":"王五", "age":22}
]
<ul>
<li>
<div class="name">姓名:张三</div>
<div class="age">年龄:18</div>
</li>
<li>
<div class="name">姓名:李四</div>
<div class="age">年龄:20</div>
</li>
<li>
<div class="name">姓名:王五</div>
<div class="age">年龄:22</div>
</li>
</ul>
2. mustache 基本使用
2.1 mustache 库简介
2.2 mustache 库基本使用
2.2.1 引入 mustache 库
2.2.2 循环对象数组
Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
var templateStr = `
<ul>
// 模版语法
{{#arr}} // 从#开始循环arr这个数组,name、sex、age都是arr数组里的值
<li>
<div class="name">姓名:{{name}}</div>
<div class="age">年龄:{{age}}</div>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{"name":"张三", "age":18},
{"name":"李四", "age":20},
{"name":"王五", "age":22}
]
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container');
container.innerHTML = domStr;
</script>
2.2.3 不循环
Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
var templateStr = `
<h1>我是{{name}},今年{{age}}岁</h1>
`
var data = {
name: '张三',
age: 18
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container');
container.innerHTML = domStr;
</script>
2.2.4 循环简单数组
Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
var templateStr = `
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
`
var data = {
arr: ['A', 'B', 'C']
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container');
container.innerHTML = domStr;
</script>
2.2.5 数组嵌套
Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
var templateStr = `
<ul>
{{#arr}}
<li>
{{name}}的爱好是:
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{'name': '小明', 'age': 12, 'hobbies': ['游泳', '羽毛球']},
{'name': '小红', 'age': 11, 'hobbies': ['编程', '写作文', '看报纸']},
{'name': '小强', 'age': 13, 'hobbies': ['打台球']},
]
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container');
container.innerHTML = domStr;
</script>
2.2.6 布尔值
Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
var templateStr = `
{{#m}}
<h1>你好</h1>
{{/m}}
`
var data = {
m: false
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container');
container.innerHTML = domStr;
</script>
3. mustache 底层核心原理
3.1 正则表达式思路
- 在简单情况下,可以用正则表达式实现
- 但当情况复杂时,正则表达式的思路就不行了
var templateStr = `
<h1>我是{{name}},今年{{age}}岁</h1>
`
var data = {
name: '张三',
age: 18
}
function render(templateStr, data) {
return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, $1) {
return data[$1];
});
}
var result = render(templateStr, data);
console.log(result);
3.2 mustache 库的原理
- Mustache 库底层重点做了两件事
将模版字符串编译为 tokens 形式 将 tokens 结合数据,解析为 DOM 字符串
3.2.1 什么是 tokens
// 模版字符串
<h1>我是{{name}},今年{{age}}岁</h1>
// tokens
[
["text", "<h1>我是"],
["name", "name"],
["text", ",今年"],
["name", "age"],
["text", "岁</h1>"]
]
3.2.2 循环情况下的 tokens
- 当模版字符串中有
循环 存在时,它将被编译为嵌套更深的 tokens
// 模版字符串
<div>
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
</div>
// tokens
[
["text", "<div><ul>"],
["#", "arr", [
["text", "<li>"],
["name", "."],
["text", "</li>"]
]],
["text", "<ul></div>"]
]
3.2.3 双重循环情况下的 tokens
// 模版字符串
<div>
<ol>
{{#students}}
<li>
学生
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ol>
</div>
// tokens
[
["text", "<div><ol>"],
["#", "students", [
["text", "<li>学生"],
["name", "name"],
["text", "的爱好是<ol>"],
["#", "hobbies", [
["text", "<li>"],
["name", "."],
["text", "</li>"],
]],
["text", "</ol></li>"],
]],
["text", "</ol></div>"]
]
4. 手写 mustache 库
4.1 构建项目
4.1.1 使用 webpack 和 webpack-dev-server 构建
- Mustache 官方库使用
rollup 进行模块化打包 - 但是我这里使用
webpack ( webpack-dev-server ) 进行模块化打包
webpack 能更方便的在浏览器中实时调用程序 ( 相比于 nodejs 控制台,浏览器控制台更好用 ) - 生成库是
UMD 的,这意味着它可以同时在 nodejs 环境中使用,也可以在浏览器环境中使用
4.1.2 webpack.config.js 文件
const path = require('path');
module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'bundle.js'
},
devServer: {
contentBase: path.join( dirname, "www"),
compress: false,
port: 8080,
publicPath: "/xuni/"
}
};
4.2 整体结构
<body>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
<script>
var templateStr = `
<div>
<ul>
{{#students}}
<li class="myli">
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ul>
</div>
`;
var data = {
students: [
{ 'name': '小明', 'hobbies': ['编程', '游泳'] },
{ 'name': '小红', 'hobbies': ['看书', '弹琴', '画画'] },
{ 'name': '小强', 'hobbies': ['锻炼'] }
]
};
var domStr = test_TemplateEngine.render(templateStr, data);
console.log(domStr);
var container = document.getElementById('container');
container.innerHTML = domStr;
</script>
</body>
window.test_TemplateEngine = {
render(templateStr, data) {
var tokens = parseTemplateToTokens(templateStr);
var domStr = renderTemplate(tokens, data);
return domStr;
}
};
4.3 具体方法
4.3.1 如何将模版字符串变为 tokens
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr;
this.pos = 0;
this.tail = templateStr;
}
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
this.pos += tag.length;
this.tail = this.templateStr.substring(this.pos);
}
}
scanUtil(stopTag) {
const pos_backup = this.pos;
while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++;
this.tail = this.templateStr.substring(this.pos);
}
return this.templateStr.substring(pos_backup, this.pos);
}
eos() {
return this.pos >= this.templateStr.length;
}
};
tokens 嵌套
- 利用数据结构–栈
- 扫描到 # 号进栈,扫描到 / 号出栈
nestTokens.js
export default function nestTokens(tokens) {
var nestedTokens = [];
var sections = [];
var collector = nestedTokens;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case '#':
collector.push(token);
sections.push(token);
collector = token[2] = [];
break;
case '/':
sections.pop();
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
};
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';
export default function parseTemplateToTokens(templateStr) {
var tokens = [];
var scanner = new Scanner(templateStr);
var words;
while (!scanner.eos()) {
words = scanner.scanUtil('{{');
if (words != '') {
let isInJJH = false;
var _words = '';
for (let i = 0; i < words.length; i++) {
if (words[i] == '<') {
isInJJH = true;
} else if (words[i] == '>') {
isInJJH = false;
}
if (!/\s/.test(words[i])) {
_words += words[i];
} else {
if (isInJJH) {
_words += ' ';
}
}
}
tokens.push(['text', _words]);
}
scanner.scan('{{');
words = scanner.scanUtil('}}');
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('}}');
}
return nestTokens(tokens);
}
4.3.2 如何将 tokens 数组变为 DOM 字符串
# 标记的 tokens,需要递归处理 下标为2的小数组注意
lookup.js
export default function lookup(dataObj, 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];
};
import lookup from './lookup.js';
import renderTemplate from './renderTemplate.js';
export default function parseArray(token, data) {
var v = lookup(data, token[1]);
var resultStr = '';
for(let i = 0 ; i < v.length; i++) {
resultStr += renderTemplate(token[2], {
...v[i],
'.': v[i]
});
}
return resultStr;
};
import lookup from './lookup.js';
import parseArray from './parseArray.js';
export default function renderTemplate(tokens, data) {
var resultStr = '';
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token[0] == 'text') {
resultStr += token[1];
} else if (token[0] == 'name') {
resultStr += lookup(data, token[1]);
} else if (token[0] == '#') {
resultStr += parseArray(token, data);
}
}
return resultStr;
}
|