模板引擎是将数据变为视图最优雅的解决方案,如下可以通过左侧数据最终在网页上渲染成右侧dom视图
历史上出现的数据变为视图的方法有四种:纯DOM法、数组join法、ES6反引号法、mustache模板引擎法,可以说越来越优雅。
如下数据采用各种方法渲染
<div>
<ul class="list">
ul>
div>
<script>
let students = [{
name: '小明',
age: 18
},
{
name: '小红',
age: 19
},
{
name: '小华',
age: 20
},
];
script>
1.纯DOM法
纯DOM法就是通过对DOM的原生操作(获取节点、创建节点、添加节点)来将数据拼接添加
<script>
for (let i = 0; i < students.length; i++) {
let list = document.querySelector('.list');
let li = document.createElement('li');
let p1 = document.createElement('p');
p1.innerText = '我是' + students[i].name;
let p2 = document.createElement('p');
p2.innerText = '我今年' + students[i].age + '岁';
list.appendChild(li);
li.appendChild(p1);
li.appendChild(p2);
}
</script>
如上可以看出像渲染一个简单的students数据却非常繁琐,通过获取节点,然后创建节点,添加节点。
2.数组join法
曾经非常流行,通过数组join的方法,将模板字符串通过数组分段join连接起来,从而可以达到一种看似换行的效果
<script>
let list = document.querySelector('.list');
for (let i = 0; i < students.length; i++) {
list.innerHTML += [
' ' ,
' 我是'
+ students[i].name + '',
' 我今年'
+ students[i].age + '岁了',
' '
].join('')
}
</script>
3.ES6反引号法
在ES6中新增了``反引号,可以直接换行输入字符串。
let list = document.querySelector('.list');
for (let i = 0; i < students.length; i++) {
list.innerHTML += `
我是
${students[i].name}
我今年
${students[i].age}岁了
`
}
4.mustache渲染引擎
通过mustache渲染引擎可以将{{}}内属性进行解析渲染
这里通过bootcdn复制mustache.js,
import Mustache from "../libjs/mustache.js"
let templateStr = `
{{#students}}
我是{{name}}
我今年{{age}}岁了
{{/students}}
`;
let list = document.querySelector('.list');
let domStr = Mustache.render(templateStr, data);
list.innerHTML = domStr;
如上通过mustache内的render进行渲染,传入模板字符串与数据,根据识别#号可以进行循环,不需要for进行循环
这里通过配置webpack、webpack-cli、webpack-dev-server进行模块化开发
1.index.js中声明全局引擎TemplateEngine对象,其中添加render函数
2.在index.html中调用TemplateStr.render,传入模板字符串templateStr和渲染数据data
3.render内部调用parseTemplateToTokens
4.parseTemplateToTokens内部调用scanner类方法(
scanUntil:读取非{{}}内容
scan:跳过{{}}字符串
utail:更新tail
exo:是否到达终点
)得到读取后的所有字符串并分类型(text,name,#,/)加入tokens数组
5.调用nestTokens方法传入tokens将tokens转为嵌套数组
6.调用renderTemplate传入tokens,data渲染数据,lookup(
传入数据,字符串属性,可判断.类型的字段
返回对应的值
)返回domStr
7.将最终得到domStr添加到innerHtml
主文件index.js
声明全局引擎TemplateEngine对象,内部包含render方法,传入参数templateStr模板字符串与数据data。
import parseTemplateToTokens from "./parseTemplateToTokens"
import renderTemplate from "./renderTemplate"
//全局提供TemplateEngine
window.TemplateEngine = {
render(tempateStr, data) {
//调用parseTemplateToTokens方法,将模板字符串转为Tokens
let tokens = parseTemplateToTokens(tempateStr);
console.log(tokens);
//调用renderTemplate方法,将tokens转为domStr
let domStr = renderTemplate(tokens, data);
return domStr;
}
}
parseTemplateToTokens.js
import Scanenr from "./Scanner";
import nestTokens from "./nestTokens";
export default function parseTemplateToTokens(templateStr) {
//实例化一个扫描器,构造提供一个模板字符串参数
let scanner = new Scanenr(templateStr);
let tokens = [];
let words;
while (!scanner.exo()) {
words = scanner.scanUntil('{{')
//去掉空格,但是不能去掉标签内的空格
let isInLable = false; //是否在标签内
let _words = '';
for (let i = 0; i < words.length; i++) {
let w = words[i];
if (w === '<') isInLable = true;
if (w === '>') isInLable = false;
if (!/\s/g.test(w)) {
_words += w
} else {
if (isInLable) {
_words += w
}
}
}
tokens.push(['text', _words])
scanner.scan('{{')
words = scanner.scanUntil('}}')
if (words.length > 0) {
let word0 = words[0];
if (word0 === '#') {
tokens.push(['#', words.substring(1)])
} else if (word0 === '/') {
tokens.push(['/', words.substring(1)])
} else {
tokens.push(['name', words])
}
}
scanner.scan('}}')
}
return nestTokens(tokens);
}
内部先调用scanner类读取模板字符串
scanner.js
export default class Scanenr {
constructor(templateStr) {
this.templateStr = templateStr;
this.pos = 0;
this.tail = this.utail()
}
//过渡{{}}
scan(etag) {
this.pos += etag.length;
this.tail = this.utail()
}
//过渡返回非{{}}字符串
scanUntil(etag) {
const pre_pos = this.pos;
while (!this.exo() && this.tail.indexOf(etag) != 0) {
this.pos++;
this.tail = this.utail()
}
return this.templateStr.substring(pre_pos, this.pos)
}
//更新tail
utail() {
return this.templateStr.substring(this.pos)
}
//判断是否到达终点
exo() {
return this.pos >= this.templateStr.length
}
}
将Tokens变为可循环嵌套的Tokens
nestTokens.js
//将tokens变为嵌套tokens
export default function nestTokens(tokens) {
//返回结果的数组
let nestedTokens = [];
//栈结构 用来存放迭代数,栈顶的token表示当前操作token
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);
collector = token[2] = [];
break;
case '/':
sections.pop();
//如果栈中还有则将collector指向上一级循环的添加数组
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
collector.push(token);
break;
}
}
return nestedTokens
}
进行数据渲染
renderTemplate.js
import lookup from "./lookup";
export default function renderTemplate(tokens, data) {
let resultStr = '';
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case 'text':
resultStr += token[1]
break;
case 'name':
resultStr += lookup(data, token[1])
break;
case '#':
let key = lookup(data, token[1]);
for (let j = 0; j < key.length; j++) {
resultStr += renderTemplate(token[2], key[j])
}
}
}
return resultStr
}
根据对象,字符串查找数据
lookup.js
/*
功能:将含有a.b.c类型的数据进行查找
如{
a:{
b:{
c:3
}
}
}
则a.b.c=3
*/
export default function lookup(data, valueStr) {
if (valueStr === '.') return data
if (valueStr.indexOf('.') != -1) {
let values = valueStr.split('.');
let start = 0;
if (values[0] === 'item') start = 1;
let temp = data;
for (let i = start; i < values.length; i++) {
temp = temp[values[i]]
}
return temp
}
return data[valueStr];
}
最后renderTempate返回值即可得到最终的domStr,再将domStr通过innerHtml解析即可