mustache 基本语法

mustache是胡子的意思,他的标记{{}}非常像胡子 所以就叫这个名字,这个语法也被vue沿用,但是他不能和vue一样在里边写表达式。
下边使用的都是简化版的mustache也就是最后写出来的样子。原版直达链接->https://github.com/janl/mustache.js

渲染变量

1
2
3
4
5
6
const data = {
name:'Joker',
age:18
}
const templateStr = `我叫{{name}},今年{{age}}岁`;
document.body.innerHTML = Mustache.render(templateStr,data);

渲染简单数组

1
2
3
4
5
6
7
8
9
10
11
const data = {
user:['Joker','Pink','xyooio']
}
const templateStr = `
<ul>
{{#user}}
<li>{{.}}</li>
{{/user}}
</ul>
`;
document.body.innerHTML = Mustache.render(templateStr,data);

渲染对象数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const data = {
user: [
{ name: 'Joker', age: 18 },
{ name: 'Pink', age: 19 },
{ name: 'xyooio', age: 20 },
]
}
const templateStr = `
<ul>
{{#user}}
<li>我叫{{name}},今年{{age}}岁</li>
{{/user}}
</ul>
`;
document.body.innerHTML = Mustache.render(templateStr,data);

mustache的底层核心

根据上边写的基本语法就可以看出来,Mustache就是将我们的模板变成结合数据变为模板字符串。而这个过程中必然不是直接转化的 哈哈哈哈,会有一个中间过程,它最后产出的东西叫做tokens
那么他的流程就是 将模板字符串编译为tokens,然后再结合数据将tokens解析成dom字符串
token是什么样子呢

1
const templateStr = `我叫{{name}},今年{{age}}岁`;

这样的模板字符串解析为编译为tokens之后的样子是这样的。

1
2
3
4
5
6
7
[
["text","我叫"],
["name","name"],
["text",",今年"],
["name","age"],
["text","岁"]
]

可以看到他是一个二维数组, 第一项是类型,第二项是要替换的值。text直接放进去,name就读取数据源了的值

  • text:包括标签和换行空格等,只要不是在{{}}里都在这块
  • name:代表要替换的基础数据
  • #:循环数组,从#到/之间的部分都为这一个行

看一个循环的tokens,第三项就是要循环的tokens

1
2
3
4
5
6
7
8
9
10
11
[
["text","<ul>"],
["#","user",
[
["text","<li>"],
["name","."],
["text","</li>"]
]
],
["text","</ul>"]
]

mustache的实现

创建项目

简单一点,不用webpack创建了,就简单的创建几个文件就行了
创建一个目录Mustache;
创建Mustache/index.html文件 用来引用js,来看效果
编写如下内容,简单的引入无其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!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>

</body>
<script type="module">
import Mustache from "./src/index.js";
</script>

</html>

创建Mustache/src/index.js文件 项目的入口文件
编写如下内容,简单的抛出无其他

1
2
3
4
5
export default {
render(templateStr, data) {

}
}

思路

看着上边的流程就可以看出来 需要基本需要编写两个函数1. 将模板字符串编译为tokens的函数,2. 将tokens解析为dom字符串的函数。

  1. 看第一个函数,将模板编译为tokens,那就需要一个扫描器,也就是是我们一样的眼睛 哈哈哈,一个一个的去看(实现scanner类)
    • 属性
      • pos 指针 记录现在看到第几个字符了
      • tail 当前指针本身以及之后的字符串
      • templateStr 模板字符串
    • 方法
      • scan 跳动指针,用于跳过标识
      • scanUntil 传入一个标识,当指针走到这个位置了停止扫描,然后返回走过的内容
      • eos 指针是不是已经走完了整个字符串
  2. scanner类已经给我们返回我们需要的内容了,那下来根据这些内容生成tokens数组
  3. 上边的数组是没有将循环的tokens折叠进第三项的,所以还需要一个方法去折叠
  4. tokens合并数据解析为dom字符串

实现scanner

创建Mustache/src/Scanner.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export default class Scanner{
constructor(templateStr) {
// templateStr 传进来的模板呗
this.templateStr = templateStr;
// 指针必然是从0开始
this.pos = 0;
// 当前指针本身以及之后的字符串 没走之前就是全部的模板
this.tail = templateStr;
}
/**
* @param tag 需要跳过的标记
* @description 让扫描器跳过指定字符
* */
scan(tag){
this.pos += tag.length;
this.tail = this.templateStr.substring(this.pos);
}
/**
* @param tag 扫描器扫描到什么字符停止
* @return 扫描出来过的字符
* @description 从指针位置开始扫描 直到扫描到指定字符停止 并返回扫描过的字符
* */
scanUntil(tag){
// 记住是从哪里开始扫描的
const startPos = this.pos;
// 在没有扫描完而且还没有扫描到指定字符时候一直扫描
while(this.eos() || this.tail.indexOf(tag) !== 0){
// 每扫描一次 指针加1
this.pos++;
// 这个东西也相继往后移动一个字符
this.tail = this.templateStr.substring(this.pos);
}
// 循环结束 那指定是扫完整个模板或者扫描到指定字符了,那无论那种情况 处理都是一样的
return this.templateStr.substring(startPos,this.pos);
}
eos(){
// 指针大于等于字符串长度那必然是走完了
return this.pos >= this.templateStr.length;
}
}

实现模板转tokens

上边的Scanner类已经可以找跳过指定字符也可以获取到两个标记之间的内容了,那下来就简单了 循环读取跳过就好了呀。
创建Mustache/src/toTokens.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import Scanner from "./Scanner.js";// 指定是需要这个类的 先引入

export default function toTokens(templateStr){
// 实例化一个扫描器
const scanner = new Scanner(templateStr);
// 最后返回的tokens
const tokens = [];
// 每一次拿到的字符就存在这个里边
let word = '';
// 没有扫描完就一直扫描 哈哈哈哈
while (!scanner.eso()){
// 这块返回的是{{之前的内容,也就是类型为text的数据
word = scanner.scanUntil('{{');
// 他不是空字符串的话就存起来 是的话 那就过吧
word && tokens.push(['text',word]);
// 刚刚是读取到了{{ 在最后的dom中不需要这个东西 所以指针往后挪一下
scanner.scan('{{');
// {{和}}之间的内容 类型就可能为 name 或者是 # /那根据第一个字符判断一下
word = scanner.scanUntil('}}');

// 这块能写法可以很好地优化一下
if(word){
if(word[0] === '#'){
tokens.push(['#',word.substring(1)]);
}else if(word[0] === '/'){
tokens.push(['/',word.substring(1)]);
}else {
tokens.push(['name',word]);
}
}
// 同样 跳过}}
scanner.scan('}}');
}
return tokens;
}

按顺序的话 应该是在写Scanner类之前写这块的代码 哈哈哈哈
现在的话 已经可以看到初步的token了 如果没有循环的话 再加上后边我们要写的 解析dom的方法 已经可以处理了。

折叠我们循环的部分

Mustache/src/toTokens.js创建nestTokens方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function nestTokens(tokens){
// 折叠后的tokens
const nestedTokens = [];
// 我们以后要操作的数组了 因为js的对象是浅拷贝的 所有我们可以通过赋值来更改他的指向
// 刚开始我们要操作的肯定就是最后的数组 直到我们遇到了#我们再进行更改
let collector = nestedTokens;
// 用这个东西关系嵌套关系 遇到 # 就往里边push一个 遇到 /就pop一个
const sections = [];
// 遍历传进来的数组,直到他遇到 #
for(const token of tokens){
if(token[0] === '#'){
// 入栈这个在最后的数组当中靠前 所以放最后处理
sections.push(token);
// 往现在操作push进去 不然我们不知到这是一个循环呀 是吧
collector.push(token);
// 我们要修改collector的指向
collector = token[2] = [];
}else if(token[0] === '/'){
// 出栈 弹出来一个
sections.pop();
// 如果我们操作栈里边还有东西 那就取出来最后一个操作 没有就是nestedTokens
collector = sections.length === 0 ? nestedTokens : sections[sections.length - 1][2];
}else{
// 正常的话 直接放进去现在我们正在操作的tokens里边
collector.push(token);
}
}
return nestedTokens;
}

然后再修改toTokens方法

1
return nestTokens(tokens);

解析为dom字符串

创建Mustache/src/renderTemplate.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default function renderTemplate(tokens,data){
// 最后生成的dom字符串
let resultDom = '';
// 遍历tokens
for(const token of tokens){
// text类型的 原样返回
if(token[0] === 'text'){
// 拼接进去就行
resultDom += token[1];
}else if(token[0] === 'name'){
// 那么 是.的话 直就拼接data
if(token[1] === '.'){
resultDom += data;
continue;
}
// 不是的话 读取出来那个值
resultDom += eval('data.' + token[1]);
}else if(token[0] === '#'){
// 读取出来数组进行循环
const arr = eval('data.' + token[1]);
// 第三个值也是tokens 直接丢进这个函数就行了
for(const item of arr){
resultDom += renderTemplate(token[2],item)
}
}
}
return resultDom;
}

这块用的eval方法很不安全 哈哈哈 后边再更新这块的写法

最后的修改

修改 Mustache/src/index.js文件

1
2
3
4
5
6
7
8
import toTokens from "./toTokens.js";
import renderTemplate from "./renderTemplate.js";
export default {
render(templateStr, data,selector) {
const tokens = toTokens(templateStr);
document.querySelector(selector).innerHTML = renderTemplate(tokens,data)
}
}

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Mustache from "./src/index.js";
const data = {
user: [
{ name: 'Joker', age: 18,hobbies:['javascript','vue'] },
{ name: 'Pink', age: 19,hobbies:['王者荣耀','吃鸡'] },
{ name: 'xyooio', age: 20,hobbies:['双人成行','分手厨房'] },
]
}
const templateStr = `
<ul>
{{#user}}
<li>
我叫{{name}},今年{{age}}岁,我的爱好都有
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/user}}
</ul>
`;
Mustache.render(templateStr,data,'body')