前端模板引擎之mustache

前端模板引擎之mustache什么是模板引擎 概括来说 将数据按照特定的方式转化为视图 html 的一种技术 举个例子 将图中的 data 数据转化为 视图 html 结构数据 搬一下网上的概念 模板引擎 这里特指用于 Web 开发的模板引擎 是为了使用户界面与业务数据 内容 分离 而产生的

大家好,我是讯享网,很高兴认识大家。

什么是模板引擎?

概括来说:将数据按照特定的方式转化为视图(html)的一种技术。
举个例子,将图中的data数据转化为 视图(html)结构数据。
image.png
讯享网
搬一下网上的概念:模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档。
模板引擎的核心原理就是两个字:替换。将预先定义的标签字符替换为指定的业务数据,或者根据某种定义好的流程进行输出。

Mustache模板引擎

Mustache介绍

Mustache是一款非常经典的前端模板引擎,是一套轻逻辑的模板语法。它可以来处理HTML,配置文件,源代码等文件。它把模板中的标签展开成给定的映射或属性值。这里的轻逻辑是指模板里面没有if语句,else语句,for循环语句,只有模板标签。
mustache 是 “胡子”的意思,因为它的嵌入标记{ {}}非常像胡子。
mustache是最早的模板引擎库,比Vue诞生的早多了,它的底层实现机理在当时是非常有创造性的、轰动性的,为后续模板引擎的发展提供了崭新的思路。{ {}}也被Vue沿用。
在前后端分离的技术架构中,前端模板引擎是一种可以被考虑的技术选型,随着前端三大框架(Angular、React、Vue)的流行,前端的模板技术已经成了标配。Mustache的价值在于稳定性和经典。

官网:https://mustache.github.io/mustache.5.html

Mustache的安装与使用

任何能够使用JavaScript的地方都可以使用mustache.js渲染模板。包括浏览器或者像node、CouchDB 这样的服务器环境。

Mustache安装
npm install mustache --save 

讯享网
Mustache使用
简单使用

我们来看一个简单的例子

讯享网import mustache from "mustache/mustache.mjs"; let view = { 
    title: "Joe", calc: function () { 
    return 2 + 4; } }; let output = mustache.render("{ 
   {title}} spends { 
   {calc}}", view); console.log(output); // Joe spends 6 

通过上面的实例我们可以看到mustache使用{ { }}作为标记的界定符。Vue里面的双大括号语法参考了mustache的实现。mustache通过占位符来表示动态数据的位置。例如,一个{ {title}}表示一个占位符,将在渲染的过程中被对应的数据替换。数据可以是简单的变量,也可以是对象的属性。

迭代列表

上面例子只是比较简单的情况,在实际的开发中,经常会出现嵌套,循环的情况,之前说到过mustache是一套轻逻辑的模板语法,里面没有for循环,为了解决这个问题,mustache提供了{ {#condition}}...{ {/condition}}{ {#list}}...{ {/list}}的语法,我们可以根据条件和迭代列表来渲染内容。其中双括号中的#代表条件或循环的开始,/代表结束,中间的内容就代表着循环体。
再看个例子

//引入Mustacha import mustache from "mustache/mustache.mjs"; let template = `<ul> { 
    {#arr}} <li> <div>{ 
    {name}}的基本信息</div> <div> <p>姓名:{ 
    {name}}</p> <p>性别:{ 
    {sex}}</p> <p>年龄:{ 
    {age}}</p> </div> </li> { 
    {/arr}} </ul>`; let view = { 
    arr:[ { 
    name: "小王",sex: "男",age: 18}, { 
    name: "小明",sex: "男",age: 25}, { 
    name: "小刘",sex: "男",age: 30} ] } let output = mustache.render(template,view); console.log(output); 

控制台输出:反引号法可以保留空格
image.png
从上面可以看到 mustache以一种非常简洁的方式生成了动态的内容。

Mustache解析数据规律

mustache引擎执行过程

通过上面的实例可以看到我们将只要渲染的数据和模板文件传递给Mustache模板引擎,引擎会自动的解析模板文件并根据数据进行渲染,生成最终的结果。那么模板引擎是如何实现这一过程的呢?看下这张图。
image.png
在这个过程中引擎会先将模板文件编译成tokens,然后再把tokens结合数据渲染成dom结构的字符串。也就是说它主要实现了两件事

  1. 将模板文件编译成tokens形式
  2. 将tokens结合数据,生成dom的字符串

tokens是什么

在mustache模板引擎中tokens是最重要的部分,它是连接模板文件和数据的桥梁。它本质是一个JS的嵌套数组,主要功能是将模板文件转化为JS可以操作的数据格式。

无循环嵌套情况

举个例子,如果模板文件是<h1>Today { {title}} spends { {calc}} dollar</h1>,那它经过编译后生成的tokens就是这种嵌套数组形式。
image.png
如上图所示,模板文件实际上就是一个纯文本文件,我们可以把它当做一个长字符串。在这个长字符串中以双括号{ {}}为标记进行切割,切割后的一个个片段就是一个个token,用一个数组来存储token的信息。
image.png
如图所示,每一个token元素对应一个数组,数组第一个元素存储token的类型,主要有textname两种类型,text指代纯文本内容,不含标签,占位符和相关的控制结构。name指代{ {}}双括号中的内容。,第二个元素存储对应的内容,第三和第四个元素是token字段的开始位置和结束位置,[0,10)左闭右开。

循环嵌套情况

上面的模板文件中是不包含迭代的,对于有嵌套循环的情况,Mustache模板引擎将循环嵌套部分当做一整个token,然后再将这整个token分割为一个个的小token。如下图所示:
image.png
上图中的模板文件是用 模板字符串(反引号) 包裹的,可以保留空格和换行,所以解析后的tokens中有空格和换行符\n。据图可以看到整个循环部分被当做一个token,token数组的第一个值是#表示这个token是迭代或条件token,第二个值就是迭代的列表或条件,第三、第四个值分别表示迭代条件的开始位置与结束位置,第五个值表示迭代的内容,作为一个tokens,套入其中。第六个值则是整个迭代结束的位置。

调试mustache模板引擎

我现在安装的mustache插件是最新版本,版本号为:4.2.0,引入的js文件的路径为:node_modules/mustache/mustache.js,找到js文件中的 parseTemplate 方法,其主要作用是生成tokens,修改代码,在控制台输出其生成的tokens。
image.png
引入修改后的js文件,测试下

讯享网//引入Mustacha import mustache from "mustache/mustache.js"; //创建一个模板 let template = `<ul> { 
  
    
  {#arr}} <li> <div>{ 
  
    
  {name}}的基本信息</div> <div> <p>姓名:{ 
  
    
  {name}}</p> <p>性别:{ 
  
    
  {sex}}</p> <p>年龄:{ 
  
    
  {age}}</p> </div> </li> { 
  
    
  {/arr}} </ul>`; let view = { arr:[ { name: "小王",sex: "男",age: 18}, { name: "小明",sex: "男",age: 25}, { name: "小刘",sex: "男",age: 30} ] } let output = mustache.render(template,view); 

控制台输出:
image.png
mustache模板引擎将模板字符串解析成tokens的规则我们已经了解清楚了,接下来尝试手动实现这个过程。

Mustache实现模板文件到tokens的转变

根据上面的介绍,我们已经了解到Mustache模板引擎解析数据的规律,接下来我们就需要用代码来实现模板文件到tokens的转变。

简单情况

最终目标

先看一下模板文件,我们需要将{ {...}}中的内容提取出来,并将其转化为二维数组。在本次实现过程中token对应数组['text','<h1>Today',0,10]第三,第四元素未起到作用,所以我们只实现前两位['text','<h1>Today']
image.png

实现思路分析

1,想要提取模板字符串中的{ {}}包裹的数据,必须要遍历模板字符串,来逐个查找。
2,设置一个指针pos,用来标记遍历模板字符串时的进度位置。
image.png
3,指针pos将模板字符串分割成两部分:已扫描字符串,待扫描字符串(包含pos指向的字符)。
image.png
4,指针pos向右移动,直到首次找到待扫描字符串前两位字符是{ { 时暂停扫描或未找到直接扫描完成。
image.png
5,如若匹配到待扫描字符串前两位字符是{ { ,并将此次扫描的字符串加入数组。注意,在{ { 符号前面的字符串的类型为text
image.png
6,存储数据后继续扫描,并直接将指针pos前移两位。
image.png
7,继续扫描,直到首次找到待扫描字符串前两位字符是}}时暂停本次扫描或未匹配到直接扫描完毕。
image.png
8,找到匹配元素后,将本次扫描数据存入数组中。并将指针pos后移两位。 注意在{ {}}之前的字符串类型为name
image.png
9,继续扫描,依次循环执行第4,5,6,7,8步的操作,直到字符串扫描结束。

代码实现

创建Scanner类,主要用作扫描模板字符串。

/ * Scanner 类主要作用是扫描模板字符串 * 以'{ 
   {' 和 '}}'分隔,将字符串分离开来。 */ export default class Scanner{ 
    constructor(template){ 
    //存储模板信息 this.template = template; //初始标记位 this.pos = 0; //待扫描字符串 this.tail = this.template; } / * 匹配 { 
   { }} 等串,并使指针pos前进2位, * @param re * @returns {string|*} */ scan(re){ 
    this.startIndex = this.pos; //匹配匹配待字符串中是否有 re let match = this.tail.match(re); //如果没有匹配到或匹配位置不是在头部,返回空字符串 if(!match || match.index !== 0){ 
    return ""; } //获取被匹配的字符 re let string = match[0]; //待匹配字符串去掉被匹配的字符 re this.tail = this.tail.substring(string.length); //pos指针前进被匹配字符的长度 this.pos += string.length; //返回被匹配字符re return string; } / * 扫描模板字符串 * @param re * @returns {string} */ scanUntil(re){ 
    //search方法,用于检索字符串中指定的子串,或检索与正则表达式相匹配的子串, //返回子串第一次出现的位置,没有匹配到则返回-1 //index 是匹配子串的位置, match是被被扫描的字符串 let index = this.tail.search(re),match; switch(index){ 
    //未匹配到,待匹配字符串被扫描完毕 case -1: match = this.tail; this.tail = ""; break; //在字符串头部匹配到 case 0: match = ""; break; default: match = this.tail.substring(0,index); this.tail = this.tail.substring(index); } //指针pos前进扫描字符的长度 this.pos += match.length; //返回扫描的字符串 return match; } / * 判断模板字符串是否扫描完成 * @returns {boolean} */ eos(){ 
    return this.tail == ''; } } 

创建parseTemplateToTokens方法,将模板字符串转化为tokens。

讯享网//引入扫描器 import Scanner from "./Scanner"; export default function parseTemplateToTokens(template){ 
    let tokens = []; //创造一个扫描器实例 let scanner = new Scanner(template); let word; while(!scanner.eos()){ 
    //匹配首次出现的{ 
   {,并获取匹配到的字符串 word = scanner.scanUntil('{ 
   {'); //将匹配字符串存入数组 if(word != ""){ 
    tokens.push(['text',word]); } //指针pos后移 scanner.scan('{ 
   {'); word = scanner.scanUntil('}}'); if(word !=""){ 
    tokens.push(['name',word]); } scanner.scan('}}'); } return tokens; } 

测试:

import parseTemplateToTokens from "./parseTemplateToTokens"; let template = "<h1>Today { 
   {title}} spends { 
   {calc}} dollar</h1>"; let tokens = parseTemplateToTokens(template); console.log(tokens); 

控制台打印:
image.png

嵌套循环情况

最终目标

模板文件中有嵌套循环的部分,即将{ {#...}}{ {/...}}之间的模板代码作为一个token存储起来,如图所示:
image.png
循环部分对应的token类型区别text和name,以#区分,内容存储在一个数组里面,作为token里面的第三个元素。循环嵌套部分的代码按照无嵌套循环的情况处理。

实现思路分析

以之前简单情况为基础,在其上面继续深入。以之前的处理逻辑,转换的结果如下
image.png
根据循环部分的规则,token的类型应该为#,我们需要做一下处理,将循环部分与非循环部分区分开。对parseTemplateToTokens方法做一下更改。

讯享网import Scanner from "./Scanner"; export default function parseTemplateToTokens(template){ 
    ... while(!scanner.eos()){ 
    ... //对{ 
   {}}内的数据做判断,是循环部分还是普通情况 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; } 

更改后测试结果为:
image.png
接下来,我们需要将循环的部分放到一个token里面,如下图所示:
image.png
将方框内的数据转化为一个数组,可以利用栈的思想,遇#入栈,遇/出栈。
实现思路过程:
创建三个数组:
nestTokens 主要用来存储修改后的tokens,
sections数组作为栈,存储循环,条件数据。
collector数组作为搜集器,搜集循环里面的数据。
初始时,nestTokens和collector指向同一个数组
image.png
开始循环,正常情况,遇到text,name类型,使用collector.push(),方法添加进数组。
image.png
遇到#号,第一步,先将数据进栈。第二步,通过collector将数据添加到数组中。
image.png
第三步,创建一个新数组存储循环部分数据,并将新数组作为类型为#数据的第三个元素,同时,将collector指向新数组。
image.png
继续遍历,执行之前的操作
image.png
遇到#还是执行之前的操作
image.png
遇到/类型的token数据,表示一个循环结束,其之前的状态如下
image.png
在这步需要执行的操作有:将section最近加入的元素弹出,并将collector指向上一层。这里指回上一层,是根据section里面的数据来判断的。
image.png
之后,继续执行,得到最终结果
image.png

代码实现

创建nestTokens方法,实现对tokens的压缩。

export default function nestTokens(tokens){ 
    //创建一个数组,存储修改后的tokens let nestTokens = []; //创建一个栈,存储循环,条件数据 let sections = []; //创建一个搜集器,搜集循环里面的数据 let collector = nestTokens; for(let i = 0; i<tokens.length; i++){ 
    let token = tokens[i]; switch(token[0]){ 
    case "#": //入栈 sections.push(token); //收集器collector收集数据 collector.push(token); //改变收集器collector指向,之后可以将循环内数据存放到token中 collector = token[2] = []; break; case "/": //遇到/就出栈 sections.pop(); //每次出栈说明该层循环遍历完成,改变collector的指向,将其指回上一层 collector = sections.length>0? sections[sections.length-1][2]:nestTokens; break; default: collector.push(token); break; } } return nestTokens; } 

在parseTemplateToTokens中引入 nesTokens方法

讯享网import Scanner from "./Scanner"; import nestTokens from "./nestTokens"; export default function parseTemplateToTokens(template){ 
    ... return nestTokens(tokens); } 

测试结果

let template = `<ul> { 
    {#arr}} <li> <div>{ 
    {name}}的基本信息</div> <div> <p>姓名:{ 
    {name}}</p> <p>性别:{ 
    {sex}}</p> <p>年龄:{ 
    {age}}</p> <p> 爱好:{ 
    {#hobbies}} <span> { 
    {.}} </span> { 
    {/hobbies}} </p> </div> </li> { 
    {/arr}} </ul>`; let data = { 
    arr:[ { 
    name: "小王",sex: "男",age: 18,hobbies:["篮球","羽毛球"]}, { 
    name: "小明",sex: "男",age: 25,hobbies: ["王者荣耀","决斗连接"]}, { 
    name: "小刘",sex: "男",age: 30,hobbies: ["冲浪","滑雪"]} ] }; import parseTemplateToTokens from "./parseTemplateToTokens"; let tokens = parseTemplateToTokens(template); console.log(tokens); 

image.png
至此,我们完成了从模板文件到tokens的转变,只剩下将tokens与数据结合生成对应的html。

Mustache实现tokens与数据的结合

讯享网export default function renderTemplate(tokens,data){ 
    let templateStr = ""; for(let i = 0;i<tokens.length;i++){ 
    let token = tokens[i]; switch(token[0]){ 
    case "text": templateStr += token[1]; break; case "name": templateStr += lookup(data,token[1]); break; case "#": case "^": templateStr += parseArray(token,data); break; default:break; } } return templateStr; } / * 将token转换成字符串 * @param token * @param data */ function parseArray(token,data){ 
    let tmpStr = ""; let childrenTokens = token[2]; let childData = []; if(!data.hasOwnProperty(token[1])){ 
    return tmpStr; }else{ 
    childData = data[token[1]]; for(let i=0; i<childData.length; i++){ 
    tmpStr += renderTemplate(childrenTokens,childData[i]); } } return tmpStr; } / * 取出data中对应的数据值 * @param data * @param valueName */ function lookup(data,valueName){ 
    let temp = data; if(typeof temp == 'string' && valueName == '.'){ 
    return temp; } let nameArr = valueName.split("."); if(nameArr.length >0){ 
    for(let i = 0;i<nameArr.length;i++){ 
    temp = temp[nameArr[i]]; } return temp; } } 
import parseTemplateTOTokens from "./parseTemplateToTokens"; import renderTemplate from "./renderTemplate"; export default class Mustache{ 
    constructor(template,data){ 
    this.template = template; this.data = data; } render(){ 
    const { 
   template,data} = this; //获取token let tokens = parseTemplateTOTokens(template); //将token与数据结合 let templateStr = renderTemplate(tokens,data); return templateStr; } } 

将字符串挂载到界面上

讯享网<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"></div> <script> let template = `<ul> { 
      {#arr}} <li> <div>{ 
      {name}}的基本信息</div> <div> <p>姓名:{ 
      {name}}</p> <p>性别:{ 
      {sex}}</p> <p>年龄:{ 
      {age}}</p> <p>爱好:{ 
      {#hobbies}}<span> { 
      {.}} </span>{ 
      {/hobbies}}</p> </div> </li> { 
      {/arr}} </ul>`; let data = { 
      title: "Joe", calc:"6", mentality:{ 
      positive:"good" }, arr:[ { 
      name: "小王",sex: "男",age: 18,hobbies:["篮球","羽毛球"]}, { 
      name: "小明",sex: "男",age: 25,hobbies: ["王者荣耀","决斗连接"]}, { 
      name: "小刘",sex: "男",age: 30,hobbies: ["冲浪","滑雪"]} ] }; setTimeout(()=>{ 
      let mustache = new Mustache(template,data); let node = document.getElementById('app'); node.innerHTML = mustache.render(); },500) </script> </body> </html> 

界面显示:
image.png

小讯
上一篇 2025-02-08 22:39
下一篇 2025-01-04 18:45

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/13178.html