Vue源码之mustache模板引擎

本文深入探讨了Vue中的mustache模板引擎工作原理,通过tokens概念解析模板字符串,并展示了如何手动实现一个简单的mustache模板引擎。内容包括模板字符串到tokens的转换、循环嵌套处理、数据与模板的结合以及最终生成DOM字符串的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue源码之mustache模板引擎

mustache库的工作机理

tokens

tokens是一个JS的嵌套数组,是模板字符串的JS表示

模板字符串

 <h1> 我买了一个{{thing}},好{{mood}}啊 </h1> 

tokens(每个数组的第零项都是第一项的数据类型)

 [
 ["text", "<h1> 我买了一个"],    //token
 ["name", "thing"],            //token
 ["text", ",好"],              //token
 ["name", "mood"],             //token
 ["text", "啊</h1>"],          //token
 ] 

当模板字符串中有循环存在时,他将被编译为嵌套更深的tokens

 <div>
   <ul>
   {{#arr}}
     <li>{{.}}</li>
   {{/arr}}
   </ul>
 </div> 

tonkes

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

因此mustache库只需要做两件事

0.将模板字符串编译为tokens形式
1.将tokens结合数据,解析为dom字符串

手写一个简单的mustache模板引擎

初始化文件夹

 npm init 

安装依赖包

 npm i webpack@4 webpack-cli@3 webpack-dev-server@3 

配置webpack

 const path = require('path');
 ​
 module.exports = {
   //模式为开发模式
   mode: 'development',
   entry: './src/index.js',
   output: {
     filename: 'bundle.js',
     path: path.resolve(__dirname, 'dist')
 },
   //配置webpack_dev_server
   devServer: {
     //静态文件根目录
     contentBase: path.join(__dirname, "www"),
     //端口号
     port: 8081,
 }
 }; 

配置入口文件

www/index.html

引入一个html模板字符串,使用定义的模板引擎将模板字符串转换为html字符串

 <!DOCTYPE html>
 <html lang="en">
 <head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>mustache</title>
 </head>
 <body>
   <div id="container">Hello world</div>
   <script src="/xuni/bundle.js"></script>
   <script>
     //模板字符串
     var templateStr = `
       <ul>
         {{#students}}
           <li>
             <div class="hd"> {{name}} 的基本信息</div>
             <div class="bd">
               <p>姓名:{{name}}</p>
               <p>年龄:{{age}} </p>
               <p>性别:{{sex}} </p>
               <h2>爱好</h2>
               {{#hobbies}}
                 <h3>{{.}}</h3>
               {{/hobbies}}
             </div>
           </li>
         {{/students}}  
       </ul> 
     `
     //模板数据
     var data = {
       students: [
       {"name": "小明", "age": 12, "sex": "男", "hobbies": ["游泳", "跑步"]},
       {"name": "小红", "age": 11, "sex": "女", "hobbies": ["游泳", "跑步"]},
       {"name": "小强", "age": 13, "sex": "男", "hobbies": ["游泳", "跑步"]}
     ]
   };
  //mustache模板引擎
     var domStr = myMustache.render(templateStr, data);
  //获取元素节点
     var container = document.getElementById('container');
     //将模板字符串插入该节点中
     container.innerHTML = domStr;
   </script>
 </body>
 </html> 
src/index.js

入口 JS 文件:在window属性上绑定一个myMustache属性,在myMustache属性上定义一个render(templateStr, data)方法,该方法需要传入模板字符串和对应的数据,其中parseTemplateToTokens()方法可以将模板字符串转换为一个tokens数组,renderTemplate() 方法可以将tokens数组中的模板那变量与对应的数据进行替换,再将替换完之后的tokens数组重新拼接成模板字符串返回。

 import parseTemplateToTokens from "./parseTemplateToTokens";
 import renderTemplate from "./renderTemplate";
 ​
 // @ts-ignore
 window.myMustache = {
   render(templateStr, data) {
     var tokens = parseTemplateToTokens(templateStr);
     templateStr = renderTemplate(tokens, data);
     return templateStr;
 }
 } 

class Scanner

Scanner类:可以构造一个扫描类的实例对象,其中 scna(tag) 函数可以将字符串中为 tag 的字符跳过,输出 tag 之后的字符串;scanUtil(stopTag)函数可以将上一个stopTag和当前stopTag之间的字符串进行输出。

 /* Scanner类:识别'{{'和’}}‘ 两边和之间的内容并将其包裹成相应的数组 */
 ​
 export default class Scanner {
   constructor(templateStr) {
     this.templateStr = templateStr;
     this.pos = 0;
     this.tail = templateStr;
 }
 ​
   scan(tag) {
     if (this.tail.indexOf(tag) == 0) {
       this.pos += tag.length;
       return this.templateStr.substr(this.pos);
   }
 }
 ​
   scanUtil(stopTag) {
     var pos_back = this.pos;
     while (this.pos < this.templateStr.length && this.tail.indexOf(stopTag) != 0) {
       this.pos++;
       this.tail = this.templateStr.substr(this.pos);
   }
     return this.templateStr.substring(pos_back, this.pos);
 }
 } 

parseTemplateToTokens(templateStr)

在parseTemplateToTokens()函数中实例化一个Scanner对象,配合scan()和 scanUtil()可以将模板字符串转换为一个tokens数组,最后再调用nestTokens(tokens)函数可以在tokens数组中嵌套一个tokens数组,实现数据的循环嵌套。

 /* 将传入的字符串变为tokens数组 */
 ​
 import nestTokens from "./nestTokens";
 import Scanner from "./Scanner";
 ​
 export default function parseTemplateToTokens(templateStr) {
   var tokens = [];
   var scanner = new Scanner(templateStr);
   while (scanner.pos < templateStr.length) {
     var words = scanner.scanUtil('{{');
     if (words) {
       tokens.push(['text', words]);
   }
     scanner.scan('{{')
     var 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);
 } 

nestTokens(tokens)

将传过来的二维数组折叠成更高维的数组,实现数组的循环嵌套

 /*将 # 与 / 之间的数组进行折叠 */
 export default function nestTokens(tokens) {
   var nestTokens = [];
   var sections = [];
   var collector = nestTokens;
   for (let i = 0; i < tokens.length; i++) {
     var token = tokens[i];
     switch (token[0]) {
       case '#':
         collector.push(token);
         sections.push(token);
         collector = token[2] = [];
         break;
       case '/':
         sections.pop();
         collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens;
         break;
     
       default:
         collector.push(token);
         break;
   }
 }
   return nestTokens;
 } 

nestTokens(tokens)函数输出的nestTokens数组

 [
   ["text","<ul>"],
   ["#","students",[
       ["text","<li><div class="hd"> "],
       ["name","name"],
       [ "text"," 的基本信息</div><div class="bd"><p>姓名:"],
       ["name","name" ],
       ["text","</p><p>年龄:"],
       ["name","age"],
       ["text"," </p><p>性别:"],
       ["name", "sex"],
       ["text"," </p><h2>爱好</h2>"],
       ["#","hobbies",[
              ["text","<h3>"],
              ["name","."],
              ["text","</h3>"]
          ]
      ],
      ["text","</div></li>"]
 ] 

renderTemplate(tokens, data)

将折叠之后的tokens数组中的模板变量用data中的真实数据进行替换后,重新拼接为html字符串,配置praseArray(tokens, data)函数使用递归算法,将tokens数组中嵌套的tokens数组也进行替换拼接处理。

 import parseArray from "./parseArray";
 ​
 /* 将tokens数组转换为模板字符串 */
 export default function renderTemplate(tokens, data) {
   var resultStr = '';
   for (let i = 0; i < tokens.length; i++) {
     if (tokens[i][0] == 'text') {
       resultStr += tokens[i][1];
   } else if (tokens[i][0] == 'name') {
       var v = tokens[i][1] == '.' ? data : data[(tokens[i][1])];
       resultStr += v;
   } else {
       resultStr += parseArray(tokens[i], data)
   }
 }
   return resultStr;
 } 

praseArray(tokens, data)

配合renderTemplate()函数使用递归算法输出拼接完成的模板字符串

 import renderTemplate from "./renderTemplate";
 ​
 /* 处理tokens中的循环遍历 */
 export default function parseArray(tokens, data) {
   var templateStr = '';
   /* 遍历数据 */
   for (let i = 0; i < data[tokens[1]].length; i++) {
     templateStr += renderTemplate(tokens[2], data[tokens[1]][i]);
 }
   
   return templateStr;
 } 

输出resultStr字符串

 <ul>
   <li>
     <div class="hd"> 小明 的基本信息</div>
     <div class="bd">
        <p>姓名:小明</p>
        <p>年龄:12 </p>
        <p>性别:男 </p>
        <h2>爱好</h2>
        <h3>游泳</h3>
        <h3>跑步</h3>
     </div>
   </li>
   <li>
      <div class="hd"> 小红 的基本信息</div>
      <div class="bd">
        <p>姓名:小红</p>
        <p>年龄:11 </p>
        <p>性别:女 </p>
        <h2>爱好</h2>
        <h3>游泳</h3>
        <h3>跑步</h3>
      </div>
   </li>
   <li>
      <div class="hd"> 小强 的基本信息</div>
      <div class="bd">
         <p>姓名:小强</p>
         <p>年龄:13 </p>
         <p>性别:男 </p>
         <h2>爱好</h2>
         <h3>游泳</h3>
         <h3>跑步</h3>
      </div>
    </li>
 </ul> 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值