mustache 模板引擎核心原理
参考视频:Vue 源码解析系列课程
系列笔记:
大纲:
- 什么是模板引擎
- mustache 基本使用
- mustache 的底层核心机理
- 带你手写 mustache 库
本章源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/SGG_TemplateEngine
数据 -> 视图
数据变为视图的方法:
- 纯 DOM 法:非常笨拙,没有实战价值
- 数组 join 法:曾经非常流行
- ES6 的反引号:ES6 的语法糖,很好用
- 模板引擎:最优雅的解决方案
数据准备:
var arr = [
{ "name": "小明", "age": 12, "sex": "男" },
{ "name": "小红", "age": 11, "sex": "女" },
{ "name": "小强", "age": 13, "sex": "男" }
];
纯 DOM 法:十分麻烦,了解一下即可
var list = document.getElementById('list');
for (var 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 + '的基本信息';
// 创建bd这个div
let bdDiv = document.createElement('div');
bdDiv.className = 'bd';
// 创建三个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);
// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
oLi.appendChild(hdDiv);
// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
oLi.appendChild(bdDiv);
// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
list.appendChild(oLi);
}
数组 join 法:第一个想到的人很厉害,强行让 for 里面的代码具有层次可读性
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('')
}
ES6 的反引号:JavaScript 自身也在快速的发展
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].sex}</p>
<p>年龄:${arr[i].age}</p>
</div>
</li>
`;
}
最优雅的解决方案:模板引擎。
mustache 库
mustache 官方 git:https://github.com/janl/mustache.js
mustache 是“胡子”的意思,因为它的嵌入标记
{{ }}
非常像胡子
mustache 是最早的模板引擎库,比 Vue 诞生早很多。它的底层实现机理在当时非常有创造性、轰动性、为后续模板引擎的发展提供了崭新的思路。
BootCDN - Bootstrap 中文网开源项目免费 CDN 加速服务
基本使用
循环对象数组:
不循环,渲染数据对象:
循环简单数组:
数组可嵌套:
布尔值控制是否渲染:
底层核心机理
正则表达式实现?
比较简单的情况下,是可以用正则表达式实现的。
js 中字符串的 replace() 的第二个参数,可以是一个函数,其中
$x
代表第x
个子表达式匹配到的文本
参考:JavaScript replace() 方法
// 模板字符串
var templateStr = '<h1>我买了一个{{thing}},花了{{money}}元,好{{mood}}</h1>';
// 数据
var data = {
thing: '白菜',
money: 5,
mood: '激动'
};
// 渲染函数
function render(templateStr, data) {
// 该例中只有一个子表达式,使用 $1 可以分别捕获到 'thing', 'money', 'mood'
return templateStr.replace(/\{\{(\w+)\}\}/g, function(findStr, $1) {
return data[$1];
})
}
console.log(render(templateStr, data))
// <h1>我买了一个白菜,花了5元,好激动</h1>
当情况复杂时,无法使用正则表达式的思路来实现,比如以下模板字符串:
<div>
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
</div>
原理 - tokens
mustache 库的机理:
mustache 库底层重点做的两件事:
- 将模板字符串编译成 tokens 形式
- 将 tokens 结合数据,解析为 dom 字符串
tokens 是什么?
- 一个 JS 的嵌套数组,即 模板字符串的 JS 表示
- 它是 “抽象语法树”、“虚拟节点” 等的开山鼻祖
对于以下模板字符串:
<h1>我买了一个{{thing}},好{{mood}}啊</h1>
生成的 tokens:注意 tokens 解析的是字符串,所以 html 标签也是当字符串对待的
[
["text", "<h1>我买了一个"],
["name", "thing"],
["text", ",好"],
["name", "mood"],
["text", "啊<h1>"],
]
模板解析原理:
循环情况下的 tokens:
当循环是双重的,那么 tokens 会更深一层:
修改源码来观察 tokens:在源码中进行以下修改,即可在控制台输出 tokens
return nestTokens(squashTokens(tokens));
var tokens = nestTokens(squashTokens(tokens));
console.log(tokens);
return tokens;
手写实现
本章源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/SGG_TemplateEngine
webpack 和 webpack-dev-server 构建
模块化打包工具有 webpack (webpack-dev-server)、rollup、Parcel 等
Mustache 官方使用 rollup 进行模块化打包,我们使用 webpack (webpack-dev-server) 进行模块化打包
- webpack 开发体验很好,有热更新功能,可以在浏览器中实时调试程序
- 相比 nodejs,浏览器的控制台更好用(可以点击展开数组等)
- 生成库是 UMD 的,这意味着它可以同时在 nodejs 环境中使用,也可以在浏览器环境中使用。
实现 UMD 不难,只需要一个 “通用头” 即可?
参考:前端模块化发展(CommonJs、AMD、CMD、UMD、ESM)
# 构建项目,一路回车
npm init
# 注意版本可能会有兼容问题,最好使用以下版本
npm install -D webpack@4
npm install -D webpack-dev-server@3
npm i -D webpack-cli@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/"
}
};
源码
源码学习注意事项
- 学习源码时,源码思想要借鉴,而不要抄袭。要能够发现源码中书写的精彩的地方
- 将独立的功能拆写为独立的 js 文件中完成,通常是一个独立的类,每个单独的 功能必须能独立的 “单元测试”
- 应该围绕中心功能,先把主干完成,然后修剪枝叶
- 功能并不需要一步到位,功能的拓展要一步步完成,有的非核心功能甚至不需实现
整体架构::
使用效果:
var templateStr = `
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>性别:{{sex}}</p>
<p>年龄:{{age}}</p>
</div>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{ "name": "小明", "age": 12, "sex": "男" },
{ "name": "小红", "age": 11, "sex": "女" },
{ "name": "小强", "age": 13, "sex": "男" },
]
}
var domStr = SGG_TemplateEngine.render(templateStr, data);
var container = document.getElementById('container')
container.innerHTML = domStr
scan 与 scanUntil
源码:https://gitee.com/szluyu99/vue-source-learn/blob/master/SGG_TemplateEngine/src/Scanner.js
Scanner 类中的两个方法:
scan
就是走过指定内容,没有返回值scanUntil
让指针进行扫描,直到遇到指定内容结束,并且能否返回结束之前路过的文字
对于 我买了一个{{thing}},好{{mood}}啊
- 使用
scanUntil('{{')
会返回我买了一个
: - 使用
scan('{{')
会跳过{{
- 然后使用
scanUntil('}}')
会返回thing
- 使用
scan('}}')
会跳过}}
- 重复以上操作
parseTemplateToTokens
parseTemplateToTokens 用于将模板字符串变为 tokens 数组
简单情况:
<h1>我买了一个{{thing}},好{{mood}}啊</h1>
[
["text", "<h1>我买了一个"],
["name", "thing"],
["text", "好"],
["name", "mood"],
["text", "啊</h1>"],
]
复杂情况:
相比简单情况,复杂情况中有零散的 tokens 嵌套起来
renderTemplate
源码:https://gitee.com/szluyu99/vue-source-learn/blob/master/SGG_TemplateEngine/src/renderTemplate.js
renderTemplate 用于将 tokens 数组变为 dom 字符串(结合数据)
简单情况:
var templateStr = `我买了一个{{thing}},好{{mood}}啊,我考了{{my.score}}分!`;
var data = {
thing: '苹果',
mood: '开心',
my : {
score: 100
}
};
复杂情况:
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': ['锻炼'] }
]
};