学习Vue的mustache语法-mustache模板引擎

学习地址 : https://www.bilibili.com/video/BV1EV411h79m?vd_source=a81826692f4afea80764f4048dc1ae0a

 代码地址 :

手写mustache模板: 一个简化版的mustache


  • 什么是模板引擎

模板引擎是将数据变为视图的最优解

将数据变为视图有很多种方法:

        比如纯DOM法:

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

         数组的JOIN方法:

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

        ES6的反引号 :

<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>
  • mustache模板引擎

mustache是最早的模板引擎,比Vue诞生要早得多,它的底层机理在当时是非常具有创造性,轰动性的,为后续的模板引擎发展提供了崭新的思路

mustache是'胡子'的意思,它的嵌入标记{{ }}非常像胡子,也可以叫它大胡子语法,{{ }}语法也被Vue沿用

  • mustache处理过程

tokens就是一个js的嵌套数组,也就是模板字符串JS的表现形式

首先,我们需要先将模板字符串编译为tokens的形式:

 然后,我们需要将自己定义的数据与tokens结合,变为dom:

 当模板字符串有嵌套的情况时,它将被编译为嵌套的tokens:

  • 手写简化版mustache库

目录结构 :

 

首先,我们需要先将模板字符串变为tokens :

 将模板字符串变为tokens需要几个步骤:

第一创建模板字符串,并且调用render函数 :

// 模板字符串
    var templateStr = `
    <div>
      <ul>
        {{#students}}
          <li>
            学生{{name}}的爱好是
            <ul>
              {{#hobbies}}
                <li>{{.}}</li>
              {{/hobbies}}
            </ul>
          </li>
        {{/students}}
      </ul>
    </div>
    `;
    // 数据
    var data = {
      students: [
        { 'name': '小明', 'hobbies': ['游泳', '健身'] },
        { 'name': '小红', 'hobbies': ['足球', '篮球', '羽毛球'] },
        { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] }
      ]
    }
    // 调用render
    var domStr = awei_TemplateEngline.render(templateStr, data)

render函数接受了两个参数,一个是模板字符串,还有一个是数据 :

// 全局提供awei_TemplateEngline对象
window.awei_TemplateEngline = {
  // 渲染方法
  render(templateStr, data) {
    console.log('render函数被调用,我们需要让Scanner工作');
    
    // 实例化一个扫描器,构造式提供一个模板字符串参数
    // 这个扫描器针对模板字符串工作
    var scanner = new Scanner(templateStr)
  }
}

现在,我们要想一下如何才能识别模板字符串中的{{ }},在mustache库中,用到了一个扫描器,这个扫描器是一个Scanner类 ,那么他是如何工作的呢?

 他用到了两个指针,当第二个指针遇到{{时,scanUntil需要收集在{{之前的文字,并且通过scan路过{{

 代码实现 :

// 扫描器类
export default class Scanner {
  // 通过 new 命令创建对象实例时,自动调用constructor方法
  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.substr(this.pos)
    }
  }

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

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

现在,我们已经完成了对模板字符串的扫描,接下来,我们可以在创建一个模块,将模块命名为parseTemplateToTokens,他的功能就是用来将模板字符串编译为tokens,并且我们将刚才在render函数中调用的Scanner放到parseTemplateToTokens中 :

render函数:

// 全局提供awei_TemplateEngline对象
window.awei_TemplateEngline = {
  render(templateStr, data) {
    // 调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
    var tokens = parseTemplateToTokens(templateStr)
  }
}

parseTemplateToTokens函数 :

import Scanner from './Scanner'

/**
 * 将模板字符串变为tokens数组
 */
export default function parseTemplateToTokens(templateStr) {
  var tokens = []
  // 创建扫描器
  var scanner = new Scanner(templateStr)
  var words
  while (!scanner.eos()) {
    // 收集开始标记出现之前的文字
    words = scanner.scanUntil('{{')
    if (words != '') {
      // 存起来,去掉空格
      tokens.push(['text', words.replace(/\s{1,}(<)|(>)\s{1,}/g, '$1$2')])
    }
    // 过双大括号
    words = scanner.scan('{{')
    // 收集开始标记出现之前的文字
    words = scanner.scanUntil('}}')
    if (words != '') {
      // 这个words就是{{}}中间的东西,判断一下首字符
      if (words[0] == '#') {
        // 从下标为一得项开始存,因为下标为0的项是#
        tokens.push(['#', words.substring(1)])
      } else if (words[0] == '/') {
        // 从下标为一得项开始存,因为下标为0的项是/
        tokens.push(['/', words.substring(1)])
      } else {
        // 存起来
        tokens.push(['name', words])
      }
    }
    // 过双大括号
    words = scanner.scan('}}')
  }
}

现在,我们写的parseTemplateToTokens其实是有问题的,他并不能实现嵌套tokens,那么接下来我们就需要将零散的tokens嵌套起来,在写这个方法之前,我们需要再看一下别的知识

  • 数据结构--栈

栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。

所以,我们折叠tokens就可以用到栈的思想 

在我们的代码中,我们需要遇见#号进栈,遇到/出栈

 下面,我们创建折叠tokens的模块 nestedTokens :

/**
 * 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为他的下标为3的项
 */
export default function nextTokens(tokens) {
  // 结果数组
  var nestedTokens = []
  // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
  var sections = []
  // 收集器,一开始指向结果数组,引用类型值,所以指向的是同一个数组
  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.push(token)
    }
  }

  return nestedTokens
}

在这个算法中,有一个很巧妙的引用类型值的运用,我们对collector的添加操作其实并不是将数据添加到 collector里面,而是添加到collector一开始指向的结果数组,collector只是起到了一个收集器的作用,收集器的指向也会发生改变,当遇见#号的时候,收集器就会指向这个token的下标为2的新数组

我们在parseTemplateToTokens引入nestedTokens :

import Scanner from './Scanner'
import nextTokens from './nestTokens' // 修改

/**
 * 将模板字符串变为tokens数组
 */
export default function parseTemplateToTokens(templateStr) {
  var tokens = []

  // 创建扫描器
  var scanner = new Scanner(templateStr)
  var words
  while (!scanner.eos()) {
    // 收集开始标记出现之前的文字
    words = scanner.scanUntil('{{')
    if (words != '') {
      // 存起来,去掉空格
      tokens.push(['text', words.replace(/\s{1,}(<)|(>)\s{1,}/g, '$1$2')])
    }

    // 过双大括号
    words = scanner.scan('{{')
    // 收集开始标记出现之前的文字
    words = scanner.scanUntil('}}')
    if (words != '') {
      // 这个words就是{{}}中间的东西,判断一下首字符
      if (words[0] == '#') {
        // 从下标为一得项开始存,因为下标为0的项是#
        tokens.push(['#', words.substring(1)])
      } else if (words[0] == '/') {
        // 从下标为一得项开始存,因为下标为0的项是/
        tokens.push(['/', words.substring(1)])
      } else {
        // 存起来
        tokens.push(['name', words])
      }
    }
    // 过双大括号
    words = scanner.scan('}}')
  }

  // 返回折叠收集的tokens
  return nextTokens(tokens) // 修改
}

现在,我们已经将模板字符串转换为了tokens,下面我们就需要使用tokens结合数据,生成dom字符串了

使用tokens结合数据,生成dom字符串

我们先创建一个renderTemplate模块,这个模块的功能就是让tokens数组变为dom字符串

import lookup from './lookup'
import parseArray from './parseArray'

/**
 * 函数的功能是让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
}

可以看到,我们在renderTemplate中引入了两个模块,lookup 和 parseArray

lookup的功能是为了防止数据中出现a.b.c的形式的,在{{ }}语法中,我们可以写成{{students.name}}的形式,但是在JS中是不认识.形式的,所以我们需要lookup来帮我们处理 :

/**
 * 功能是可以在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]
}

parseArray的功能是 : 当我们遇见#号的时候,token下标为2的那一项又会是一个数组,所以我们需要用到递归的思路来解决这个问题,他接受两个参数,一个是当前#的这一项token,第二个是数据 :

import lookup from './lookup'
import renderTemplate from './renderTemplate'

/**
 * 处理数组,结合renderTemplate实现递归
 * 注意:这个函数收的是一个token
 * 递归的次数由data决定
 *
 */
export default function parseArray(token, data) {
  // 得到整体数据data中这个数组要使用的部分
  var v = lookup(data, token[1])
  // 结果字符串
  var resultStr = ''
  // 遍历v数组
  for (let i = 0; i < v.length; i++) {
    resultStr += renderTemplate(token[2], {
      // 是v[i]的展开
      ...v[i],
      // 补充一个.属性
      '.': v[i]
    })
  }
  return resultStr
}

接下来我们就可以在render函数中调用renderTemplate,将tokens数组变为dom字符串 : 

import parseTemplateToTokens from './parseTemplateToTokens'
import renderTemplate from './renderTemplate'

//全局提供awei_TemplateEngline对象
window.awei_TemplateEngline = {
  // 渲染方法
  render(templateStr, data) {
    // 调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
    var tokens = parseTemplateToTokens(templateStr)

    // 调用renderTemplate函数,让tokens数组变为dom字符串
    var domStr = renderTemplate(tokens, data)
    return domStr
  }
}

最后,我们在index.html中调用render,渲染上树 :

<body>
  <div id="container"></div>
  <script src="/xuni/bundle.js"></script>
  <script>
    // 模板字符串
    var templateStr = `
    <div>
      <ul>
        {{#students}}
          <li>
            学生{{name}}的爱好是
            <ul>
              {{#hobbies}}
                <li>{{.}}</li>
              {{/hobbies}}
            </ul>
          </li>
        {{/students}}
      </ul>
    </div>
    `;
    // 数据
    var data = {
      students: [
        { 'name': '小明', 'hobbies': ['游泳', '健身'] },
        { 'name': '小红', 'hobbies': ['足球', '篮球', '羽毛球'] },
        { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] }
      ]
    }
    // 调用render
    var domStr = awei_TemplateEngline.render(templateStr, data)
    // 渲染上树
    var container = document.getElementById('container')
    container.innerHTML = domStr
  </script>
</body>
  • 总结

在mustache的源码中,还有Context类和Writer类,Context类里面有一个缓存机制,Writer类是我们手写的把tokens变成dom,但我们写的是简化版的,所以就不需要顾虑这么多了,我觉得nestedTokens模块中的栈思想的算法确实是很巧妙,也非常值得去学习,这篇文章也是我对于学习mustache的总结,在B站白嫖是真舒服啊!

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值