来自尚硅谷的课程笔记 课程链接[尚硅谷邵山欢(考拉老师)Vue之mustache模板引擎] (opens new window) 加入大量的注释以及改写
# 1. 模板引擎的介绍
# 1.1 模板引擎是什么?
模板引擎是将数据data变为视图view(html)的解决方案
数据: 视图: Vue的解决方案
<li v-for="item in arr"></li>
# 1.2 模板引擎是怎么来的?(发展历史)
# 1. 使用原生的DOM操作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>01_数据变为视图-纯DOM法</title>
</head>
<body>
<ul id="list"></ul>
<script>
var arr = [
{ name: '小明', age: 12, sex: '男' },
{ name: '小红', age: 11, sex: '女' },
{ name: '小强', age: 13, sex: '男' },
]
var list = document.getElementById('list')
for (let i = 0; i < arr.length; i++) {
// 每遍历一项,都要用 DOM 方法去创建 li 标签
let oLi = document.createElement('li')
// 创建 hd 这个 div
let hdDiv = document.createElement('div')
hdDiv.className = 'hd'
hdDiv.innerText = arr[i].name + '的基本信息'
oLi.appendChild(hdDiv)
// 创建 bd 这个 div
let bdDiv = document.createElement('div')
bdDiv.className = 'bd'
oLi.appendChild(bdDiv)
// 创建 3 个 p 标签
let p1 = document.createElement('p')
p1.innerText = '姓名:' + arr[i].name
bdDiv.appendChild(p1)
let p2 = document.createElement('p')
p2.innerText = '年龄:' + arr[i].age
bdDiv.appendChild(p2)
let p3 = document.createElement('p')
p3.innerText = '性别:' + arr[i].sex
bdDiv.appendChild(p3)
// 创建的节点是孤儿节点,所以必须要上树才能让用户看见
list.appendChild(oLi)
}
</script>
</body>
</html>
# 2. 使用数组中的join方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>02_数据变为视图-数组join法</title>
</head>
<body>
<ul id="list">
</ul>
<script>
var arr = [
{ name: '小明', age: 12, sex: '男' },
{ name: '小红', age: 11, sex: '女' },
{ name: '小强', age: 13, sex: '男' },
]
var list = document.getElementById('list')
// 遍历 arr 数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for (let i = 0; i < arr.length; i++) {
list.innerHTML += [
'<li>',
' <div class="hd">' + arr[i].name + '的信息</div>',
' <div class="bd">',
' <p>姓名:' + arr[i].name + '</p>',
' <p>年龄:' + arr[i].age + '</p>',
' <p>性别:' + arr[i].sex + '</p>',
' </div>',
'</li>'
].join('')
}
</script>
</body>
</html>
# 3. 使用ES6反引号的方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>03_数据变为视图-ES6反引号法</title>
</head>
<body>
<ul id="list">
</ul>
<script>
var arr = [
{ name: '小明', age: 12, sex: '男' },
{ name: '小红', age: 11, sex: '女' },
{ name: '小强', age: 13, sex: '男' },
]
var list = document.getElementById('list')
// 遍历 arr 数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for (let i = 0; i < arr.length; i++) {
list.innerHTML += `
<li>
<div class="hd">${arr[i].name}的基本信息</div>
<div class="bd">
<p>姓名:${arr[i].name}</p>
<p>年龄:${arr[i].age}</p>
<p>性别:${arr[i].sex}</p>
</div>
</li>
`
}
</script>
</body>
</html>
# 2. mustache基本使用
# 2.1 mustache 库的简介
- mustache 官方 git:https://github.com/janl/mustache.js (opens new window)
- mustache 中文翻译是 “胡子”
- mustache 是最早的模板引擎库,非常有
创造性的、轰动性的
# 引入 mustache 库
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.1.0/mustache.js"></script>
# mustache 的模板语法
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p>性别:{{sex}}</p>
</div>
</li>
{{/arr}}
</ul>
# 1. 最简单的情况——不循环对象数组
<div id="container"></div>
<h1></h1>
<script>
var templateStr = `
<h1>我买了一个{{thing}},好{{mood}}啊</h1>
`
var data = {
thing: '华为手机',
mood: '开心'
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
# 2. 循环最简单的数组
<div id="container"></div>
<h1></h1>
<script>
var templateStr = `
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
`
var data = {
arr: ['苹果', '梨子', '香蕉']
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
# 3. 循环对象数组 (v-for类似)
<div id="container"></div>
<script>
var templateStr = `
<ul id="list">
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p>性别:{{sex}}</p>
</div>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{ name: '小明', age: 12, sex: '男' },
{ name: '小红', age: 11, sex: '女' },
{ name: '小强', age: 13, sex: '男' },
]
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
# 4. 循环嵌套对象数组和简单数组
<div id="container"></div>
<h1></h1>
<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>
# 5. 控制元素的显示与隐藏——布尔值
true显示 false不显示
<div id="container"></div>
<h1></h1>
<script>
var templateStr = `
{{#m}}
<h1>哈哈哈</h1>
{{/m}}
`
var data = {
m: true
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
# 6. script
模板写法
在scirpt
标签中写入模板,只要type
的值不是text/javascript
,都不会被当作js执行解析,这样可以在script
标签中写入模板,可以高亮可以自动填充
<div id="container"></div>
<!-- 模板 -->
<script type="text/template" id="mytemplate">
<ul id="list">
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p>性别:{{sex}}</p>
</div>
</li>
{{/arr}}
</ul>
</script>
<script>
var templateStr = document.getElementById('mytemplate').innerHTML
var data = {
arr: [
{ name: '小明', age: 12, sex: '男' },
{ name: '小红', age: 11, sex: '女' },
{ name: '小强', age: 13, sex: '男' },
]
}
var domStr = Mustache.render(templateStr, data)
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
# 3. mustache的原理
# 3.1 replace()方法 和 正则表达式实现最简单的模板数据填充
# 预备知识
# replace()方法
这个方法接收两个参数,第一个参数可以是一个RegExp对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数
replace()的第二个参数可以是一个函数。在只有一个匹配项时,这个函数会收到3个参数:与整个模式匹配的字符串、匹配项在字符串中的开始位置,以及整个字符串
console.log('我爱踢足球,我爱脱口秀'.replace(/我/g, function (a, b, c) {
console.log(a, b, c)
return '你'
}))
# 正则的捕获
/\}\}{(\w+)\}\}/
表示捕获{{}}中间的多个文字或数字
var templateStr = '<h1>我买了一个{{thing}},好{{mood}}的啊</h1>'
console.log(templateStr.replace(/\{\{(\w+)\}\}/g, function (a, b, c, d) {
console.log(a, b, c, d)
return '*'
}))
# 实现
<div id="container"></div>
<script>
var templateStr = '<h1>我买了一个{{thing}},花了{{money}},好{{mood}}</h1>'
var data = {
thing: '华为手机',
money: 5999,
mood: '开心'
}
// 最简单的模板引擎实现机理,利用的是正则表达式中的 replace() 方法
// replace() 的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,就是 $1 结合data对象,即可进行智能的替换
// function中的参数分别是:①findStr匹配到的部分{{thing}} ②捕获到的thing ③位置9 ④原串
function render(templateStr, data) {
return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, $1) {
return data[$1]
})
}
var domStr = render(templateStr, data)
var container = document.getElementById('container')
container.innerHTML = domStr
</script>
# 3.2 mustache 的实现原理
# 3.3 什么是 tokens?
tokens 是JS的嵌套数组,就是模板字符串的JS表示
它是抽象语法树
、虚拟节点
等的开山鼻祖
# 1. 最简单的形式
模板字符串
<h1>我买了一个{{thing}},好{{mood}}啊</h1>
tokens
[
["text", "<h1>我买了一个"],
["name", "thing"],
["text", "好"],
["name", "mood"],
["text", "啊</h1>"]
]
# 2. 循环数组情况下的 tokens
# 3. 多重循环
# 3.4 实现 mustache 库 的重点是
- 将
模板字符串
编译为tokens
- 将
tokens
结合数据data
,解析为DOM 字符串
# 4. 手写实现mustache库
# 4.1 配置webpack环境
使用 webpack 和 webpack-dev-server 构建
新建目录 YK_TemplateEngine
cd YK_TemplateEngine
npm init -yes
cnpm install -D webpack@4 webpack-cli@3 webpack-dev-server@3
新建 webpack.config.js 文件 设置代码如下
const path = require("path");
module.exports = {
mode: "development",
// 入口
entry: "./src/index.js",
// 出口
output: {
filename: "bundle.js",
},
// 配置webpack-dev-server
devServer: {
// 静态根目录
contentBase: path.join(__dirname, "www"),
// 不压缩
compress: false,
// 端口号
port: 8080,
// 虚拟打包的路径,bundle.js文件没有真正生成
publicPath: "/xuni/",
},
};
新建 src/index.js
alert('nihao')
新建 www/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>我是index.html</h1>
<script src="xuni/bundle.js"></script>
</body>
</html>
package.json 文件中新增命令:
{
"scripts": {
"dev": "webpack-dev-server",
}
}
终端运行 npm run dev
访问:http://localhost:8080/
和 http://127.0.0.1:8080/xuni/bundle.js
, 可以看到 www/index.html
和 xuni/bundle.js
文件的内容
# 4.2 实现 Scanner 扫描器类
# 预备知识 JS中字符串提取字串的方法
ECMAScript提供了3个从字符串中提取子字符串的方法:slice()
、substr()
和substring()
。
这3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。
第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。
对slice()
和substring()
而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。
对substr()
而言,第二个参数表示返回的子字符串数量。
任何情况下,省略第二个参数都意味着提取到字符串末尾。
与concat()
方法一样,slice()
、substr()
和substring()
也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值
# src/Scanner.js
/**
* 扫描器类
*/
export default class Scanner {
constructor(tempalteStr) {
this.tempalteStr = tempalteStr;
// 指针
this.pos = 0;
// 尾字符串,一开始是模板字符串原文
this.tail = tempalteStr;
}
// 扫描走过指定内容{{或}},没有返回值
scan(tag) {
if (this.tail.indexOf(tag) === 0) {
// tag 有多长,比如“{{”长度是2,就让指针向后移动多少位
this.pos += tag.length;
this.tail = this.tempalteStr.substr(this.pos);
}
}
// 让指针进行扫描,直到遇到指定{{或}}内容结束,返回结束之前路过的文字
scanUtil(stopTag) {
// 记录开始执行时 pos 的值
const post_backup = this.pos;
// 当尾字符串的开头不是stopTag时,说明还没有扫描到stopTag && 寻找到最后找不到
while (!this.eos() && this.tail.indexOf(stopTag) !== 0) {
this.pos++;
// 改变尾字符串 从当前指针到最后的全部字符
this.tail = this.tempalteStr.substr(this.pos);
}
return this.tempalteStr.substring(post_backup, this.pos);
}
// 判断指针是否已经到头 end of string 到头了
eos() {
return this.pos >= this.tempalteStr.length
}
}
# www/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>你好!!!</h1>
<script src="xuni/bundle.js"></script>
<script>
var templateStr = '<h1>我买了一个{{thing}},好{{mood}}啊</h1>'
var data = {
thing: '华为手机',
mood: '开心'
}
SGG_TemplateEngine.render(templateStr, data)
</script>
</body>
</html>
# src/index.js
import Scanner from "./Scanner";
// 全局提供YK_TemplateEngine对象
window.YK_TemplateEngine = {
// 渲染方法
render(tempalteStr, data) {
// 实例化一个扫描器,构造时提供一个参数,参数就是模板字符串
// 也就是这个扫描器就是针对这个模板字符串工作的
var scanner = new Scanner(tempalteStr);
var word
// pos指针没有到头
while (!scanner.eos()) {
word = scanner.scanUtil("{{");
console.log(word+'***');
scanner.scan("{{");
word = scanner.scanUtil("}}");
console.log(word);
scanner.scan("}}");
}
},
};
# 4.3 生成 tokens 数组
# 4.3.1 完成简单的一层数组
# src/parseTemplateToTokens
import Scanner from "./Scanner";
/**
* 将模板字符串转换成tokens数组
*/
export default function parseTemplateToTokens(tempalteStr) {
var tokens = [];
// 创建扫描器
var scanner = new Scanner(tempalteStr);
var words
// 让扫描器工作
while (!scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUtil("{{");
if (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 tokens;
}
# www/index.html
// 模板字符串
// var tempalteStr = '<h1>我买了一个{{thing}},好{{mood}}啊</h1>'
var tempalteStr = `
<div>
<ol>
{{#students}}
<li>
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ol>
</div>`
// 数据
var data = {
thing: 'phone',
mood: 'happy'
}
YK_TemplateEngine.render(tempalteStr, data)
# src/index.js
import parseTemplateToTokens from './parseTemplateToTokens'
// 全局提供YK_TemplateEngine对象
window.YK_TemplateEngine = {
// 渲染方法
render(tempalteStr, data) {
// 调用parseTemplateToTokens,可以让模板字符串变为tokens数组
var tokens = parseTemplateToTokens(tempalteStr)
console.log(tokens)
},
};
# 4.3.2 完成嵌套数组(重难点)
用栈来解决 遇见#就进栈,遇见/就出栈
# src/nestTokens.js
折叠tokens,将#xxx和/xxx之间的tokens能够折叠成Array(n),作为xxx的末尾数组 ["#", "xxx", Array(n)]
/**
* 折叠tokens,将#xxx和/xxx之间的tokens能够折叠成Array(n),作为xxx的末尾数组 ["#", "xxx", Array(n)]
* @param {*} tokens
*/
export default function nestTokens(tokens) {
// 结果数组,存储最后的嵌套数组
var nestedTokens = [];
// 栈,存放 # / 之间的tokens,栈顶的tokens数组中是当前操作的
// 遇到 # 时,入栈 将#xxx和/xxx之间的tokens能够折叠成Array(n),["#", "xxx", Array(n)]
// 遇到 / 时,出栈
var sections = [];
// 收集器数组,为 栈顶 或 结果数组 收集tokens
// 初始指向 nestedTokens结果数组,引用类型值,所以指向的是同一个数组
// 入栈后,改变指向:入栈后栈顶末尾数组 token[2]
// 出栈后,根据栈是否为空改变指向: 出栈后栈顶末尾数组 sections[sections.length - 1][2] 或 结果数组nestedTokens
var collector = nestedTokens;
for (let token of tokens) {
// 判断token的第0个元素是什么
switch (token[0]) {
case "#":
// 收集器中放入这个token(初始是nestedTokens数组,当栈中有元素时,指向栈顶token末尾数组)
collector.push(token);
// 入栈
sections.push(token);
// 改变收集器指向,指向给token添加下标为2的项
collector = token[2] = [];
break;
case "/":
// 出栈
sections.pop();
// 栈不空的情况下 改变收集器为 sections栈顶 末尾数组
// 栈空就直接指向结果数组
collector =
sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
// 普通的token
default:
// 栈中有元素,就进入栈顶末尾数组;栈中没有元素,就进入结果数组
collector.push(token);
}
}
return nestedTokens;
}
# 4.4 将tokens解析为DOM字符串
# index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>这是index.html</h1>
<div id="container"></div>
<script src="xuni/bundle.js"></script>
<script>
// 模板字符串
// var tempalteStr = '<h1>我买了一个{{thing}},好{{mood}}啊</h1>'
var tempalteStr = `
<div>
<ul>
{{#students}}
<li class="ii">
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ul>
</div>`
// 数据
var data = {
students: [{
name: '小红',
hobbies: ['羽毛球', '跆拳道']
},
{
name: '小明',
hobbies: ['足球']
},
{
name: '小王',
hobbies: ['魔术', '学习', '游戏']
}
]
}
let domStr = YK_TemplateEngine.render(tempalteStr, data)
console.log(domStr)
let container = document.getElementById('container');
container.innerHTML = domStr;
</script>
</body>
</html>
# src/index.js
import parseTemplateToTokens from "./parseTemplateToTokens";
import renderTemplate from "./renderTemplate";
// 全局提供YK_TemplateEngine对象
window.YK_TemplateEngine = {
// 渲染方法
render(tempalteStr, data) {
// 调用parseTemplateToTokens,可以让模板字符串变为tokens数组
var tokens = parseTemplateToTokens(tempalteStr);
var domStr = renderTemplate(tokens, data);
return domStr
},
};
# src/lookup.js 可以在对象中,寻找连续点符号的属性
/**
* 可以在dataObj对象中,寻找连续点符号的keyName属性 比如a.b.c {a:{b:{c:100}}}
* @param {object} dataObj
* @param {string} keyName
*/
export default function lookup(dataObj, keyName) {
// 判断keyName中有没有点符号,但不能是.本身
if (keyName.indexOf(".") !== -1 && keyName !== '.') {
let temp = dataObj; // 临时变量用于周转,一层一层找下去
let keys = keyName.split(".");
for (let key of keys) {
temp = temp[key];
}
return temp;
}
return dataObj[keyName]
}
# 简化版 src/lookup.js
export default function lookup(dataObj, keyName) {
// 只有一个元素不影响最终结果,不影响循环语句最终结果,所以不用判断keyName中有没有点符号
return keyName !== '.' ? keyName.split('.').reduce((prevValue, currentKey) => prevValue[currentKey], dataObj) : dataObj[keyName]
}
# src/renderTemplate.js
import lookup from './lookup'
import parseArray from './parseArray'
/**
* 让 tokens数组 变成 DOM字符串
* @param {array} tokens
* @param {object} data
*/
export default function renderTemplate(tokens, data) {
// 结果字符串
let resultStr = "";
for (let token of tokens) {
if (token[0] === "text") {
resultStr += token[1];
} else if (token[0] === "name") {
resultStr += lookup(data, token[1]);
} else if (token[0] === "#") {
// 递归调用 renderTemplate
resultStr += parseArray(token, data)
}
}
return resultStr;
}
# src/parseArray.js 递归调用 renderTemplate
import lookup from "./lookup";
import renderTemplate from "./renderTemplate";
/**
* 处理数组,结合renderTemplate实现递归
* 参数时token不是tokens 是一个简单的数组 ['#', 'arr', Array[n]]
*
* 递归调用renderTemplate函数,调用次数由data决定
* 比如data是这样的
* {
students: [{
name: '小红',
hobbies: ['羽毛球', '跆拳道']
},
{
name: '小明',
hobbies: ['足球']
},
{
name: '小王',
hobbies: ['魔术', '学习', '游戏']
}
]
}
* parseArray()函数要递归调用renderTemplate函数3次,数组的长度=3
*/
export default function parseArray(token, data) {
// console.log(token, data);
// 得到data中这个数组需要使用的部分
let newData = lookup(data, token[1]);
// console.log(newData);
// 结果字符串
let resultStr = '';
for (let item of newData) {
resultStr += renderTemplate(token[2], {
// 展开newData[i] 并加入 点 数组
...item,
'.': item
})
}
return resultStr;
}
# 4.5 完善空格问题
- 普通文字的空格直接去掉
- 标签中的空格不能去掉,比如
<div class="box"><></div>
不能去掉class
前面的空格
// 收集开始标记之前的文字
words = scanner.scanUtil('{{')
// 存起来
if (words !== '') {
// 判断普通文字的空格,还是标签中的空格
// 标签中的空格不能去掉,比如 <div class="box"><></div> 不能去掉class前面的空格
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 += words[i]
}
}
}
tokens.push(['text', _words])
}