美文网首页
mustache模板引擎原理

mustache模板引擎原理

作者: 强某某 | 来源:发表于2020-12-18 17:31 被阅读0次

模板引擎之前的时代

  • 纯DOM法: 非常笨拙,没有实战价值
  • 数组join法: 曾几何时非常流行,是曾经的前端必会知识
  • ES6的反引号法: ES6中新增的${a}语法糖,很好用
  • 模板引擎: 解决数据变为视图的最优雅的方法

纯DOM法

<!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>
    <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 (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);
        }
    </script>
</body>

</html>

数组join法

<!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>
    <ul id="list"></ul>

    <script>
         // var str=['A','B','C','D'].join('');//实际输出ABCD,如果不加'',则输出A,B,C,D
        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>

ES6的反引号法

<!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>
    <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].sex}</p>    
                        <p>年龄:${arr[i].age}</p>    
                    </div>    
                </li>
            `;
        }
    </script>
</body>

</html>

模板引擎:Mustache

<!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>
    <div id="container"></div>

    <!-- 模板 -->
    <script type="text/template" id="mytemplate">
        <ul>
            {{#arr}}
                <li>
                    <div class="hd">{{name}}的基本信息</div>    
                    <div class="bd">
                        <p>姓名:{{name}}</p>    
                        <p>性别:{{sex}}</p>    
                        <p>年龄:{{age}}</p>    
                    </div>
                </li>
            {{/arr}}
        </ul>
    </script>

    <script src="jslib/mustache.js"></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>
</body>
</html>

Mustache原理

mustache库不能用简单的正则表达式思路实现

  • 在较为简单的示例情况下,可以用正则表达式实现
模板字符串: <h1>我买了一个{{thing}},好{{mood}}</h1>
数据: {
    thing: '华为手机',
    mood: '开心' 
}
  • 但是当情况复杂时,正则表达式的思路肯定就不行了。比如这样的模板字符串,
    是不能用正则表达式的思路实现的
<div><ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
</div>

mustache库的机理

1.png

什么是tokens

  • tokens是一个JS的嵌套数组,说白了,就是模板字符串的JS表示
  • 它是“抽象语法树”、“虚拟节点”等等的开山鼻祖
  • 其实tokens就是编译原理中第一步,解析中的词法分析结果
模板字符串
<h1>我买了一个{{thing}},好{{mood}}啊</h1>

tokens
[ 
  ["text", "<h1>我买了一个"],
  ["name", "thing"],
  ["text", ",好"],
  ["name", "mood"],
  ["text", "啊</h1>"],
]

循环情况下的tokens

<div>
    <ul>
    {{#arr}}
    <li>{{.}}</li>
    {{/arr}}
    </ul>
</div>

[ 
    ["text", "<div><ul>"],
    ["#", "arr", [ 
        ["text", "<li>"],
        ["name", "."],
        ["text", "</li>"]
    ]],
    ["text", "</ul></div>"] 
]

双重循环情况下的tokens

  • 当循环是双重的,那么tokens会更深一层
    <div>
        <ol>
        {{ #students}}
        <li>
            学生{{ name }}的爱好是
        <ol>
                {{ #hobbies}}
                <li>{{.}}</li>
                {{/ hobbies}}
        </ol>
        </li>
        {{/ students}}
        </ol>
    </div>


    [
        ["text", "<div><ol>"],
        ["#", "students", [
            ["text", "<li>学生"],
            ["name", "name"],
            ["text", "的爱好是<ol>"],
            ["#", "hobbies", [
                ["text", "<li>"],
                ["name", "."],
                ["text", "</li>"],
            ]],
            ["text", "</ol></li>"],
        ]],
        ["text", "</ol></div>"]
    ]

mustache库底层重点要做两个事情:

  • 将模板字符串编译为tokens形式
  • 将tokens结合数据,解析为dom字符串

实现简化mustache库

  • webpack.config.js
const path=require('path');
module.exports={
    //模式:开发
    mode:'development',
    //入口文件
    entry:'./src/index.js',
    //打包出口
    output:{
        filename:'bundle.js',
    },
    devServer:{
        //静态文件根目录
        contentBase:path.join(__dirname,"www"),
        //不压缩
        compress:false,
        //端口号
        port:8080,
        //虚拟打包的路径,bundle.js文件没有真正的生成
        publicPath:'/xuni/'
    }
}
  • package.json
{
  "name": "mustachedemo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.44.2",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }
}

注意:如果想生成UMD模式的包。(这意味着它可以同时在nodejs环境中使用,也可以在浏览器环境中使用)。只需要一个“通用头”即可,百度就知道了。

代码

  • 新建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>
    <div id="container"></div>

    <script src="/xuni/bundle.js"></script>

    <script>
        // 模板字符串
        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': ['锻炼'] }
            ]
        };

        // 调用render
        var domStr = SSG_TemplateEngine.render(templateStr, data);
        console.log(domStr);

        // 渲染上树
        var container = document.getElementById('container');
        container.innerHTML = domStr;
    </script>
</body>

</html>
新建src文件夹
  • index.js
import parseTemplateToTokens from './parseTemplateToTokens.js';
import renderTemplate from './renderTemplate.js';

// 全局提供SSG_TemplateEngine对象
window.SSG_TemplateEngine = {
    // 渲染方法
    render(templateStr, data) {
        // 调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
        var tokens = parseTemplateToTokens(templateStr);
        // 调用renderTemplate函数,让tokens数组变为dom字符串
        var domStr = renderTemplate(tokens, data);
        
        return domStr;
    }
};
  • lookup.js
//此处我的第一想法是递归,实际上不需要,很精妙

/* 
    功能是可以在dataObj对象中,寻找用连续点符号的keyName属性
    比如,dataObj是
    {
        a: {
            b: {
                c: 100
            }
        }
    }
    那么lookup(dataObj, 'a.b.c')结果就是100
    不忽悠大家,这个函数是某个大厂的面试题
*/
export default function lookup(dataObj, keyName) {
    // 看看keyName中有没有点符号,但是不能是.本身
    if (keyName.indexOf('.') != -1 && keyName != '.') {
        // 如果有点符号,那么拆开
        var keys = keyName.split('.');
        // 设置一个临时变量,这个临时变量用于周转,一层一层找下去。
        var temp = dataObj;
        // 每找一层,就把它设置为新的临时变量
        for (let i = 0; i < keys.length; i++) {
            temp = temp[keys[i]];
        }
        return temp;
    }
    // 如果这里面没有点符号
    return dataObj[keyName];
};
  • nestTokens.js
/* 
    函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
*/
export default function nestTokens(tokens) {
    // 结果数组
    var nestedTokens = [];
    // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
    var sections = [];
    // 收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组
    // 收集器的指向会变化,当遇见#的时候,收集器会指向这个token的下标为2的新数组
    var collector = nestedTokens;

    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];

        switch (token[0]) {
            case '#':
                // 收集器中放入这个token
                collector.push(token);
                // 入栈
                sections.push(token);
                // 收集器要换人。给token添加下标为2的项,并且让收集器指向它
                collector = token[2] = [];
                break;
            case '/':
                // 出栈。pop()会返回刚刚弹出的项
                sections.pop();
                // 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
                collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
                break;
            default:
                // 甭管当前的collector是谁,可能是结果nestedTokens,也可能是某个token的下标为2的数组,甭管是谁,推入collctor即可。
                collector.push(token);
        }
    }

    return nestedTokens;
};
  • parseArray.js
import lookup from './lookup.js';
import renderTemplate from './renderTemplate.js';

/* 
    处理数组,结合renderTemplate实现递归
    注意,这个函数收的参数是token!而不是tokens!
    token是什么,就是一个简单的['#', 'students', [

    ]]
    
    这个函数要递归调用renderTemplate函数,调用多少次???
    千万别蒙圈!调用的次数由data决定
    比如data的形式是这样的:
    {
        students: [
            { 'name': '小明', 'hobbies': ['游泳', '健身'] },
            { 'name': '小红', 'hobbies': ['足球', '蓝球', '羽毛球'] },
            { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] },
        ]
    };
    那么parseArray()函数就要递归调用renderTemplate函数3次,因为数组长度是3
*/

export default function parseArray(token, data) {
    // 得到整体数据data中这个数组要使用的部分
    var v = lookup(data, token[1]);
    // 结果字符串
    var resultStr = '';
    // 遍历v数组,v一定是数组
    // 注意,下面这个循环可能是整个包中最难思考的一个循环
    // 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。
    for(let i = 0 ; i < v.length; i++) {
        // 这里要补一个“.”属性
        // 拼接
        resultStr += renderTemplate(token[2], {
            ...v[i],
            '.': v[i]
        });
    }
    return resultStr;
};
  • parseTemplateToTokens.js
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';

/* 
    将模板字符串变为tokens数组
*/
export default function parseTemplateToTokens(templateStr) {
    var tokens = [];
    // 创建扫描器
    var scanner = new Scanner(templateStr);
    var words;
    // 让扫描器工作
    while (!scanner.eos()) {
        // 收集开始标记出现之前的文字
        words = scanner.scanUtil('{{');
        if (words != '') {
            // 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格
            // 标签中的空格不能去掉,比如<div class="box">不能去掉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 += ' ';
                    }
                }
            }
            // 存起来,去掉空格
            tokens.push(['text', _words]);
        }
        // 过双大括号
        scanner.scan('{{');
        // 收集开始标记出现之前的文字
        words = scanner.scanUtil('}}');
        if (words != '') {
            // 这个words就是{{}}中间的东西。判断一下首字符
            if (words[0] == '#') {
                // 存起来,从下标为1的项开始存,因为下标为0的项是#
                tokens.push(['#', words.substring(1)]);
            } else if (words[0] == '/') {
                // 存起来,从下标为1的项开始存,因为下标为0的项是/
                tokens.push(['/', words.substring(1)]);
            } else {
                // 存起来
                tokens.push(['name', words]);
            }
        }
        // 过双大括号
        scanner.scan('}}');
    }

    // 返回折叠收集的tokens
    return nestTokens(tokens);
}
  • renderTemplate.js
import lookup from './lookup.js';
import parseArray from './parseArray.js';
/* 
    函数的功能是让tokens数组变为dom字符串
*/
export default function renderTemplate(tokens, data) {
    // 结果字符串
    var resultStr = '';
    // 遍历tokens
    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];
        // 看类型
        if (token[0] == 'text') {
            // 拼起来
            resultStr += token[1];
        } else if (token[0] == 'name') {
            // 如果是name类型,那么就直接使用它的值,当然要用lookup
            // 因为防止这里是“a.b.c”有逗号的形式
            resultStr += lookup(data, token[1]);
        } else if (token[0] == '#') {
            resultStr += parseArray(token, data);
        }
    }

    return resultStr;
}
  • Scanner.js
/* 
    扫描器类
*/
export default class Scanner {
    constructor(templateStr) {
        // 将模板字符串写到实例身上
        this.templateStr = templateStr;
        // 指针
        this.pos = 0;
        // 尾巴,一开始就是模板字符串原文
        this.tail = templateStr;
    }

    // 功能弱,就是走过指定内容,没有返回值
    scan(tag) {
        if (this.tail.indexOf(tag) == 0) {
            // tag有多长,比如{{长度是2,就让指针后移多少位
            this.pos += tag.length;
            // 尾巴也要变,改变尾巴为从当前指针这个字符开始,到最后的全部字符
            this.tail = this.templateStr.substring(this.pos);
        }
    }

    // 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
    scanUtil(stopTag) {
        // 记录一下执行本方法的时候pos的值
        const pos_backup = this.pos;
        // 当尾巴的开头不是stopTag的时候,就说明还没有扫描到stopTag
        // 写&&很有必要,因为防止找不到,那么寻找到最后也要停止下来
        while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
            this.pos++;
            // 改变尾巴为从当前指针这个字符开始,到最后的全部字符
            this.tail = this.templateStr.substring(this.pos);
        }

        return this.templateStr.substring(pos_backup, this.pos);
    }

    // 指针是否已经到头,返回布尔值。end of string
    eos() {
        return this.pos >= this.templateStr.length;
    }
};

相关文章

  • mustache模板引擎原理

    模板引擎之前的时代 纯DOM法: 非常笨拙,没有实战价值 数组join法: 曾几何时非常流行,是曾经的前端必会知识...

  • Mustache 模板引擎

    mustache.js 是一个简单强大的 JavaScript 模板引擎,最新版本:https://www.boo...

  • 2019-07-15Mustache学习笔记

    Mustache学习笔记 Mustache 是一款基于javascript 实现的模板引擎,类似于 Microso...

  • 【资料】Mustache 语法基础

    1、Mustache 语法基础: Mustache 是一款「logic-less(轻逻辑)」的前端模板引擎,它原本...

  • mustache模板引擎入门

    版权声明:博客转载时,请标明本博客出处:http://www.jianshu.com/p/7dc036415d39...

  • 模板引擎原理

    # 模板引擎 ## 原理 ![模板引擎原理](F:\前端学习资料\模板引擎原理.jpg) ## 使用到的正则表达式...

  • 无标题文章

    # 模板引擎 ## 原理 ![模板引擎原理](F:\前端学习资料\模板引擎原理.jpg) ## 使用到的正则表达式...

  • Vue基础教程之-模板语法和计算属性(三)

    一、模板语法 1.1 内容 Vue中的元素内容使用mustache模板引擎进行解析。https://github....

  • GRMustache 的使用

    GRMustache 是用 Objective-C 编写的 Mustache 模板引擎,适用于 MacOS Coc...

  • Mustache

    Mustache 是一个前段的模型引擎。通过Mustache模板,我们再将所需要的数据往里面填充就可以得到相应的结...

网友评论

      本文标题:mustache模板引擎原理

      本文链接:https://www.haomeiwen.com/subject/jclfnktx.html