likes
comments
collection
share

mustache 实现

作者站长头像
站长
· 阅读数 19

mustache 是什么?

       初次接触 mustache 是在 vue 中,当时还比较奇怪为什么叫 mustache。之后在探究 vue mustache 的实现原理中,才了解到是有一个叫 mustache 的库。

       mustache即模版引擎,是将数据变为视图解决方案。

    <ul>
      <li v-for="user in users" >
        姓名:{{ user.name }}
        年龄:{{ user.age }}
      </li>
    </ul>
      data: {
        users: [
          {
            id: 1,
            name: 'tom',
            age: 20,
          },
          {
            id: 2,
            name: 'jerry',
            age: 20,
          }
        ]
      },

mustache 实现

       在 vue 中通过模版引擎,我们可以非常优雅的将数据映射到视图中。

       一切技术都是在迭代中发展出来的,所以要了解 mustache 的实现原理之前,我们还需要探究数据映射到视图中的演变史。

数据映射到视图的演变史

DOM 法

  <ul id="list">

  </ul>
  <script>
    const users = [
      {
        id: 1,
        name: 'tom',
        age: 20,
      },
      {
        id: 2,
        name: 'jerry',
        age: 20,
      }
    ];
    let listEl = document.getElementById("list");
    for (let i = 0; i < users.length; i++) {
      // 创建 li 元素
      const li = document.createElement("li");
      li.innerText = '姓名:' + users[i].name + '年龄:' + users[i].age;
      // 上树
      list.appendChild(li);
    }
  </script>

数组 join 法

    const users = [
      {
        id: 1,
        name: 'tom',
        age: 20,
      },
      {
        id: 2,
        name: 'jerry',
        age: 20,
      }
    ];
    let listEl = document.getElementById("list");
    for (let i = 0; i < users.length; i++) {
      const str = [
        '<li>' +
          '姓名:' + users[i].name +
          '年龄:' + users[i].age +
        '</li>'
      ].join('');

      listEl.innerHTML += str;
    }

ES6 反引号法

    const users = [
      {
        id: 1,
        name: 'tom',
        age: 20,
      },
      {
        id: 2,
        name: 'jerry',
        age: 20,
      }
    ];
    let listEl = document.getElementById("list");
    for (let i = 0; i < users.length; i++) {

      const str  = `
      <li>
        姓名:${users[i].name}
        年龄:${users[i].age}
      </li>`

      listEl.innerHTML += str;
    }

mustache

  <div id="container">

  </div>
 <script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>


  <script>
    var templateStr = `
      <ul>
        {{ #users }}
            <li>
                姓名:{{ name }}
                年龄:{{ age }}
            </li>
        {{/users}}
      </ul>
    `;

    const data = {
      users: [
        {
          id: 1,
          name: 'tom',
          age: 20,
        },
        {
          id: 2,
          name: 'jerry',
          age: 20,
        }
      ]
    };

    var domStr = Mustache.render(templateStr, data);

    const container = document.getElementById('container');
    container.innerHTML = domStr;

  </script>

        在上面的例子中,由于 DOM 的嵌套结构不够深,所以看起来可能实现方案都可以。但是如果 DOM 树的嵌套层次比较深呢?这样的话就可以明显感受出不同。

       除了 DOM 法,其余的方法都是采用将 DOM 树转为字符串,然后利用 innterHTML 实现上树。并且 mustache 自动将数据注入到模版中。

mustache 基本使用

       在上面的代码中,我们已经引用到了 mustache,并且做了简单的使用,接下来了解一下其提供的其他语法。

数据填充

  <script>
    var templateStr = `
      <h1>{{ name }} 的年龄是{{ age }}</h1>`;

    var data = {
      name: 'tom',
      age: 20,
    };

    var domStr = Mustache.render(templateStr, data);
    const container = document.getElementById('container');
    container.innerHTML = domStr;

  </script>

循环简单数组

    var templateStr = `

        <ul>

            {{ #arr }}

                <li>

                    {{ . }}

                </li>

            {{/arr}}

        </ul>

    `;

    var data = {

        arr: ['a', 'b', 'c']

    };

    var domStr = Mustache.render(templateStr, data);

    const container = document.getElementById('container');

    container.innerHTML = domStr;

循环对象数组

    var templateStr = `

        <ul>

            {{ #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);

    const container = document.getElementById('container');

    container.innerHTML = domStr;

循环嵌套数组

var templateStr = `

    <ul>

        {{ #arr }}

            <li>

                <div class="hd">{{ name }}的基本信息</div>

                <div class="bd">

                    <p>姓名:{{ name }}</p>

                    <p>年龄:{{ age }}</p>

                    <p>爱好:

                        <ol>

                            {{ #hobbies}}

                                <li>{{.}}</li>

                            {{ /hobbies }}

                        </ol>

                    </p>

                </div>

            </li>

        {{/arr}}

</ul>

`;


var data = {

    arr: [

        { "name": "小明", "age": 12, "hobbies": ["游泳", "羽毛球"] },

        { "name": "小红", "age": 11, "hobbies": ["编程", "篮球"] },

        { "name": "小白", "age": 13, "hobbies": ["游戏"] },

    ]

};


var domStr = Mustache.render(templateStr, data);


const container = document.getElementById('container');

container.innerHTML = domStr;

</script>

条件判断

<div id="container"></div>



<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>

<script>

var templateStr = `

    {{ #res }}

        <h1>hello world</h1>

    {{ /res }}

`;



var data = {

    res: false,

};



var domStr = Mustache.render(templateStr, data);

const container = document.getElementById('container');

container.innerHTML = domStr;

</script>

mustache 核心原理

       1、将模版字符串编译为 tokens 形式。

       2、将 tokens 结合数据,解析为 dom 字符串。

       tokens 是什么?

       tokens 为模版字符串的 JS 对象的表示,其是一个嵌套的 JS 数组。虚拟节点、抽象语法树的实现都是借鉴了 tokens 的思想。

<h1>{{ name }} 的年龄是{{ age }}</h1>`;

        对应的 tokens 为

[ 
 ['text', '<h1>'],
 ['name', 'name'],
 ['text', ' 的年龄是'],
 ['name', 'age'],
 ['text', '</h1>']
]
<div>
    <ul>
        {{#arr}}
            <1i>{{.}}</1i>
        {{/arr}}
    </ul>
</div>

        对应的 tokens 为

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

手写 mustache

生成 tokens 数组

分割模版字符串

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) {

        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);

    }


    // 指针是否已经到头,返回 布尔值

    eos() {

        return this.pos >= this.templateStr.length;

    }

}

生成 tokens

import Scanner from './Scanner.js';



/**

* 将模版字符串变为 tokens 数组

* @param {*} templateStr

*/

export default function parseTemplateToTokens(templateStr) {

    let tokens = [];

    var scanner = new Scanner(templateStr);

    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;

}

折叠数组

import Scanner from './Scanner.js';


/**

* 折叠tokens,将 # 和 / 之间的 tokens 折叠起来,形成嵌套数组

* @param {*} templateStr

*/

export default function nestTokens(tokens) {

    var nestedTokens = [];

    var collector = nestedTokens;

    var sections = [];


    var token, section;

    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {

        token = tokens[i];


        switch (token[0]) {

        case '#':

        case '^':

            collector.push(token);

            sections.push(token);

            collector = token[2] = [];

            break;

        case '/':

            section = sections.pop();

            collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;

            break;

        default:

            collector.push(token);

        }

    }

    return nestedTokens;

}
import Scanner from './Scanner.js';

import nestTokens from './nestTokens.js';


/**

* 将模版字符串变为 tokens 数组

* @param {*} templateStr

*/

export default function parseTemplateToTokens(templateStr) {


    let tokens = [];

    var scanner = new Scanner(templateStr);

    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 nestTokens(tokens);

}

mustache 实现

tokens 注入数据

解析模版中属性


export default function lookup(dataObj, keyName) {

    if (keyName === '.') {

        return dataObj;

    }


    let keyArr = keyName.split('.');

    let obj = dataObj;

    for (let key of keyArr) {

        obj = obj[key];

    }

    return obj;

}

注入数据

import Scanner from './Scanner.js';

import nestTokens from './nestTokens.js';

import lookup from './lookup.js';

import { render } from 'vue';



/**

* 将 tokens 数组渲染数据后,转为 DOM 字符串

*/

export default function renderTemplate(tokens, data) {

    var domStr = '';

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

        let token = tokens[i];

        switch (token[0]) {

            case 'text':

                domStr += token[1];

                break;

            case 'name':

                domStr += lookup(data, token[1]);

                break;

            case '#':

                const tokenData = lookup(data, token[1]);

                for (let j = 0; j < tokenData.length; j++) {

                    domStr += renderTemplate(token[2], tokenData[j]);

                }

                break;

            default:

                break;

        }

    }

    return domStr;

}