什么是模板引擎?
概括来说:将数据按照特定的方式转化为视图(html)的一种技术。
举个例子,将图中的data数据转化为 视图(html)结构数据。
讯享网
搬一下网上的概念:模板引擎(这里特指用于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);
控制台输出:反引号法可以保留空格
从上面可以看到 mustache以一种非常简洁的方式生成了动态的内容。
Mustache解析数据规律
mustache引擎执行过程
通过上面的实例可以看到我们将只要渲染的数据和模板文件传递给Mustache模板引擎,引擎会自动的解析模板文件并根据数据进行渲染,生成最终的结果。那么模板引擎是如何实现这一过程的呢?看下这张图。
在这个过程中引擎会先将模板文件编译成tokens,然后再把tokens结合数据渲染成dom结构的字符串。也就是说它主要实现了两件事
- 将模板文件编译成tokens形式
- 将tokens结合数据,生成dom的字符串
tokens是什么
在mustache模板引擎中tokens是最重要的部分,它是连接模板文件和数据的桥梁。它本质是一个JS的嵌套数组,主要功能是将模板文件转化为JS可以操作的数据格式。
无循环嵌套情况
举个例子,如果模板文件是<h1>Today {
{title}} spends {
{calc}} dollar</h1>,那它经过编译后生成的tokens就是这种嵌套数组形式。
如上图所示,模板文件实际上就是一个纯文本文件,我们可以把它当做一个长字符串。在这个长字符串中以双括号{
{}}为标记进行切割,切割后的一个个片段就是一个个token,用一个数组来存储token的信息。
如图所示,每一个token元素对应一个数组,数组第一个元素存储token的类型,主要有text和name两种类型,text指代纯文本内容,不含标签,占位符和相关的控制结构。name指代{
{}}双括号中的内容。,第二个元素存储对应的内容,第三和第四个元素是token字段的开始位置和结束位置,[0,10)左闭右开。
循环嵌套情况
上面的模板文件中是不包含迭代的,对于有嵌套循环的情况,Mustache模板引擎将循环嵌套部分当做一整个token,然后再将这整个token分割为一个个的小token。如下图所示:
上图中的模板文件是用 模板字符串(反引号) 包裹的,可以保留空格和换行,所以解析后的tokens中有空格和换行符\n。据图可以看到整个循环部分被当做一个token,token数组的第一个值是#表示这个token是迭代或条件token,第二个值就是迭代的列表或条件,第三、第四个值分别表示迭代条件的开始位置与结束位置,第五个值表示迭代的内容,作为一个tokens,套入其中。第六个值则是整个迭代结束的位置。
调试mustache模板引擎
我现在安装的mustache插件是最新版本,版本号为:4.2.0,引入的js文件的路径为:node_modules/mustache/mustache.js,找到js文件中的 parseTemplate 方法,其主要作用是生成tokens,修改代码,在控制台输出其生成的tokens。
引入修改后的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);
控制台输出:
mustache模板引擎将模板字符串解析成tokens的规则我们已经了解清楚了,接下来尝试手动实现这个过程。
Mustache实现模板文件到tokens的转变
根据上面的介绍,我们已经了解到Mustache模板引擎解析数据的规律,接下来我们就需要用代码来实现模板文件到tokens的转变。
简单情况
最终目标
先看一下模板文件,我们需要将{
{...}}中的内容提取出来,并将其转化为二维数组。在本次实现过程中token对应数组['text','<h1>Today',0,10]第三,第四元素未起到作用,所以我们只实现前两位['text','<h1>Today']

实现思路分析
1,想要提取模板字符串中的{
{}}包裹的数据,必须要遍历模板字符串,来逐个查找。
2,设置一个指针pos,用来标记遍历模板字符串时的进度位置。
3,指针pos将模板字符串分割成两部分:已扫描字符串,待扫描字符串(包含pos指向的字符)。
4,指针pos向右移动,直到首次找到待扫描字符串前两位字符是{
{
时暂停扫描或未找到直接扫描完成。
5,如若匹配到待扫描字符串前两位字符是{
{
,并将此次扫描的字符串加入数组。注意,在{
{
符号前面的字符串的类型为text
6,存储数据后继续扫描,并直接将指针pos前移两位。
7,继续扫描,直到首次找到待扫描字符串前两位字符是}}时暂停本次扫描或未匹配到直接扫描完毕。
8,找到匹配元素后,将本次扫描数据存入数组中。并将指针pos后移两位。 注意在{
{}}之前的字符串类型为name。
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);
控制台打印:
嵌套循环情况
最终目标
模板文件中有嵌套循环的部分,即将{
{#...}}和{
{/...}}之间的模板代码作为一个token存储起来,如图所示:
循环部分对应的token类型区别text和name,以#区分,内容存储在一个数组里面,作为token里面的第三个元素。循环嵌套部分的代码按照无嵌套循环的情况处理。
实现思路分析
以之前简单情况为基础,在其上面继续深入。以之前的处理逻辑,转换的结果如下
根据循环部分的规则,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; }
更改后测试结果为:
接下来,我们需要将循环的部分放到一个token里面,如下图所示:
将方框内的数据转化为一个数组,可以利用栈的思想,遇#入栈,遇/出栈。
实现思路过程:
创建三个数组:
nestTokens 主要用来存储修改后的tokens,
sections数组作为栈,存储循环,条件数据。
collector数组作为搜集器,搜集循环里面的数据。
初始时,nestTokens和collector指向同一个数组
开始循环,正常情况,遇到text,name类型,使用collector.push(),方法添加进数组。
遇到#号,第一步,先将数据进栈。第二步,通过collector将数据添加到数组中。
第三步,创建一个新数组存储循环部分数据,并将新数组作为类型为#数据的第三个元素,同时,将collector指向新数组。
继续遍历,执行之前的操作
遇到#还是执行之前的操作
遇到/类型的token数据,表示一个循环结束,其之前的状态如下
在这步需要执行的操作有:将section最近加入的元素弹出,并将collector指向上一层。这里指回上一层,是根据section里面的数据来判断的。
之后,继续执行,得到最终结果
代码实现
创建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);

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