模板引擎是将数据变为视图的最优雅的解决办法。目前将数据处理为视图的方法,从时间线排列有:
纯DOM法 document.createElement()
数组 join,借助 HTMLElement.prototype.innerHTML
将字符串解析为HTML
es6 模板字符串
替代 join 函数${data}
模板引擎,vue中的就是一种模板引擎。此处解析的mustache是最早的模板引擎,因它的嵌入标记
{{}}
像胡子而命名
例如:将下图数据转为视图
// 数据
const data = {
title: '信息',
arr: [
{ name: 'Jay', age: 18},
{ name: 'Bin', age: 20}
],
}
<p>{{title}}p>
<ul>
{{#arr}}
<li>
<p>{{name}}p>
<p>{{age}}p>
li>
{{/arr}}
ul>
<p>信息p>
<ul>
<li>
<p>Jayp>
<p>18p>
li>
<li>
<p>Binp>
<p>20p>
li>
ul>
引入mustache.4.1.0.js
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div id="box">div>
<script src="./mustache4_1_0.js">script>
<script type="text/template" id="myTemplate">
<p>{{title}}</p>
<ul>
{{#arr}}
<li>
<p>{{name}}</p>
<p>{{age}}</p>
</li>
{{/arr}}
</ul>
script>
<script>
// 数据
const data = {
title: '信息',
arr: [
{ name: 'Jay', age: 18 },
{ name: 'Bin', age: 20 }
]
}
const templateStr = document.getElementById('myTemplate').innerHTML;
const domStr = Mustache.render(templateStr, data)
const box = document.getElementById('box')
box.innerHTML = domStr
script>
body>
html>
将 {{title}} 信息
转为
很简单
templateStr.replace(/\{\{(\w+)\}\}/, function (findStr, $1) {
return data[$1];
})
但遇到复杂嵌套的数据怎么解决呢?mustache 引入了 token 的概念。在 mustache.js 中打断点输出 tokens,可以看到模板被解析为下图所示 tokens
它用 text 标记普通文本,用 name 标记嵌入数据名称,并形成和模板一样的嵌套结构
根据 tokens 和 data 形成最后的字符串,下面自己实现一下。
目标实现双重嵌套
let templateStr = `
{{title}}
{{#arr}}
-
{{name}}
{{age}}
{{#num}}
{{.}}
{{/num}}
{{/arr}}
`;
const data = {
title: '信息',
arr: [
{name: 'Jay', age: 18, num: [1, 2, 3]},
{name: 'Bin', age: 20, num: [1, 2, 3]}
],
}
第一步不是直接实现嵌套结构的 tokens,先将模板转为扁平的一维 tokens.
原理:先检索{{
,将之前的数据用 text 标记,再在剩余数据检索}}
,中间数据用 name 标记,循环往复,直至结束。此处未考虑{{}}
不对称及其他情况,只是简单实现。
export default function ParseTemplateStrToTokens(templateStr) {
let stopScanUtilTag = '{{';
let stopScanTag = '}}';
let tokens = [];
scanUtil(tokens, templateStr);
return tokens;
/* 收集 name 即{{}}中间的东西 */
function scan(tokens, tail) {
let res;
let pos = tail.search(stopScanTag);
if (pos > 0) {
res = tail.substring(0, pos);
tail = tail.substring(pos + stopScanTag.length);
} else {
res = tail;
}
res = res.trim();
if (res.length > 0) {
// 用 # / 标记嵌套开始和结束
if (res[0] === '#' || res[0] === '/') {
tokens.push([res[0], res.substring(1)])
} else tokens.push(['name', res]);
}
if (pos > 0 && tail.length > 0) {
scanUtil(tokens, tail);
}
}
/* 收集 text */
function scanUtil(tokens, tail) {
let res;
let pos = tail.search(stopScanUtilTag);
if (pos > 0) {
res = tail.substring(0, pos);
tail = tail.substring(pos + stopScanUtilTag.length);
} else {
res = tail;
}
res = res.trim();
if (res.length > 0) tokens.push(['text', res]);
if (pos > 0 && tail.length > 0) {
scan(tokens, tail);
}
}
}
输出 tokens
下面将一维的 tokens 转为嵌套结构。
思想:这里用 nestTokens 存储嵌套的 tokens,并用到数据结构栈 stack。
将 tokens 分为三类: #、/、default,进行遍历
function nestTokens() {
let flag = 0; // 记录栈是否为空
let stack = [];
let nestTokens = []; // 最终嵌套tokens
tokens.forEach(token => {
switch (token[0]) {
case '#':
flag++;
stack.push(token)
break;
case '/':
flag--;
let section = [];
let temp;
while (true) {
temp = stack.pop();
if (temp[0] === '#' && temp[1] === token[1]) break;
else section.unshift(temp);
}
temp[2] = section;
if (flag === 0) nestTokens.push(temp);
else stack.push(temp)
break
default:
if (flag === 0) nestTokens.push(token);
else stack.push(token)
}
})
return nestTokens;
}
输出 nestTokens
思想: 对 tokens 遍历,遇到嵌套结构,再针对数据遍历。递归调用。
const data = {
title: '信息',
arr: [
{name: 'Jay', age: 18, num: [1, 2, 3]},
{name: 'Bin', age: 20, num: [1, 2, 3]}
],
}
export default function renderTemplate(tokens, data) {
let domStr = '';
tokens.forEach(token => {
if (token[0] === 'text') domStr += token[1];
else if (token[0] === 'name') {
if (token[1] !== '.') domStr += data[token[1]];
else domStr += data;
} else if (token[0] === '#') {
data[token[1]].forEach(data => {
domStr += renderTemplate(token[2], data);
})
}
})
return domStr;
}
输出 domStr(未改变格式)
<p>信息p>
<ul><li>
<p>Jayp>
<p>18p><span>1span><span>2span><span>3span>li><li>
<p>Binp>
<p>20p><span>1span><span>2span><span>3span>li>ul>
import ParseTemplateStrToTokens from "./parseTemplateStrToTokens"
import RenderTemplate from "./renderTemplate";
window.templateEngine = {
render(templateStr, data) {
let tokens = ParseTemplateStrToTokens(templateStr);
let domStr = RenderTemplate(tokens, data)
return domStr;
}
}
使用
<body>
<div id="app">div>
<script>
let templateStr = `
{{title}}
{{#arr}}
-
{{name}}
{{age}}
{{#num}}
{{.}}
{{/num}}
{{/arr}}
`;
const data = {
title: '信息',
arr: [
{name: 'Jay', age: 18, num: [1, 2, 3]},
{name: 'Bin', age: 20, num: [1, 2, 3]}
],
}
let domStr = templateEngine.render(templateStr, data);
document.getElementById('app').innerHTML = domStr;
script>
body>
至此,自定义实现了 mustache 简易功能,了解了vue底层模板引擎实现的思路。