mustache模板引擎,带你了解{{小胡子}}

255 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

模板引擎是什么?

一提到动态改变试图你能够想到几种方法?一般人肯定能想到直接操作dom元素或者数组join方法,更有甚者肯定能够想到es6的反引号模板字符串,而我将介绍的是mustache模板引擎

模板引擎——一种将数据变为视图的方法。

mustache

mustache,中文名为胡子,也就是我们在vue中使用的{{}};mustache通过render函数解析 由模板字符串转化为的token数据data结合生成dom模板。

mustache基本语法

在说明他的运行机理之前我们需要先了解他的语法规则,下面通过两个方面让大家快速看懂大胡子的dom模板是如何生成的:

渲染数据

大胡子,见名知意,遇到了 {{ 即被识别为一个token,而对于有循环存在的token,编译器会将其嵌套为更深层的token数组。这里我们得学会有多少个token,以便看懂双指针查找大胡子算法(Scan和ScanUtil),在这里简单举一个例子

<div id="box"></div>
const data = {
  arr: [
    {
      name: 'Me',
      age: 18
    }
  ]
  arr1:['a','b','c']
  m:false;
}
// dom 模板
const templateStr = `
    <ul>
      {{#arr}}//识别到了!大胡子!所以直到本注释为止这里是俩token
        <li>
          <div>
            <p>//这里又是一个!里面狠狠地嵌套了...
                {{name}}
            </p>
            <p>{{age}}</p>
          </div>
        </li>
      {{/arr}}
      {{#arr1}}
          {{.}} //这样就可以遍历arr1里面的所有值
      {{/arr1}}
      {{#m}}    //由于m是一个布尔值,解析她的时候只有为真,下述才会显示
          我被显示啦!
      {{/m}}
  </ul>
`
// 通过 cdn 引入 mustache 库就有了 Mustache 对象
const domStr = Mustache.render(templateStr, data)
const box = document.getElementById('box')
box.innerHTML = domStr//如此就可以将盒子内元素编程通过函数编译之后的值啦

将字符串编译为token的过程这里举一个简单的例子

Snipaste_2022-12-04_22-00-12.jpg 至于为什么大胡子会这么被划分,我们只需要记住,mustache是不会识别这个东西是标签还是文字,统统都被解析为文本text

相信经过上述例子,我们已经基本掌握了mustache的语法了。接下来我将和大家一起学习模板字符串是如何转化为我们人工数出来的token的。

Scanner类

在这个类里面,我们主要含有两个方法,以此实现将普通的字符串转化为token,它们分别是:

  1. scan
  2. scanUntil

而在实现以上两种情况的方法的前提,我们需要在Scanner构造器中初始化对应两种情况识别的指针,也就是我们的移动探索指针和尾指针

export default class Scanner {
  constructor(templateStr) {
    this.templateStr = templateStr
    // 指针
    this.pos = 0
    // 尾巴
    this.tail = templateStr//传入模板字符串
  }
  ......

scan

主要用于识别大括号内的字符串,即——{{我是被识别的部分}}

具体如何实现被指针识别呢?我们上代码

// Scanner.js
  scan(stopTag) { 
    this.pos +=  stopTag.length // 指针跳过 stopTag,比如 stopTag 是 {{,则 pos 就会加2
    this.tail = this.templateStr.substring(this.pos) // substring 不传第二个参数直接截取到末尾,substring(start,end)方法 用于提取字符串中介于两个指定下标之间的字符
  }
}

scanUntil

主要用于识别直到大括号的字符串

scanUntil(stopTag) {//传入停止值
    const pos_backup = this.pos // 记录本次扫描开始时的指针位置
    // 当指针还没扫到最后面,并且当尾巴开头不是 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) // 返回扫描过的字符串,不包括 this.pos 处
  }
  
  // 指针是否已经抵达字符串末端,返回布尔值 eos(end of string)
  eos() {
    return this.pos >= this.templateStr.length
  }

templateStr函数

最后我们再调用这个类使用类中的方法

// parseTemplateToTokens.js
import Scanner from './Scanner.js'

// 函数 parseTemplateToTokens
export default templateStr => {
const tokens = []
const scanner = new Scanner(templateStr)
let word
while (!scanner.eos()) {
  word = scanner.scanUntil('{{')
  word && tokens.push(['text', word]) // 保证 word 有值再往 tokens 里添加
  scanner.scan('{{')
  word = scanner.scanUntil('}}')
  /** 
   *  判断从 {{ 和 }} 之间收集到的 word 的开头是不是特殊字符 # 或 /, 
   *  如果是则这个 token 的第一个元素相应的为 # 或 /, 否则为 name
   */
  word && (word[0] === '#' ? tokens.push(['#', word.substr(1)]) : 
    word[0] === '/' ? tokens.push(['/', word]) : tokens.push(['name', word]))
  scanner.scan('}}')
}
return nestTokens(tokens) // 返回折叠后的 tokens, 详见下文
} 

总结

以上流程简单来说就是render函数接收到一个模板字符串发给templateStr函数,经过双指针(移动指针pos和尾指针tail)处理,先识别胡子外的字符串,也就是scanUntil函数;然后再识别胡子内的字符串,也就是scan函数。通过语句判断到底应该调用哪个函数,如以上代码所示,直到指针走到最后一位为止字符串被完全push进token中,template的处理完成。