mustache模板引擎原理

模板引擎之前的时代

  • 纯DOM法: 非常笨拙,没有实战价值
  • 数组join法: 曾几何时非常流行,是曾经的前端必会知识
  • ES6的反引号法: ES6中新增的${a}语法糖,很好用
  • 模板引擎: 解决数据变为视图的最优雅的方法

纯DOM法





    
    
    Document



    

数组join法





    
    
    Document



    

    ES6的反引号法

    
    
    
    
        
        
        Document
    
    
    
        

      模板引擎:Mustache

      
      
      
      
          
          
          Document
      
      
      
          

      Mustache原理

      mustache库不能用简单的正则表达式思路实现

      • 在较为简单的示例情况下,可以用正则表达式实现
      模板字符串: 

      我买了一个{{thing}},好{{mood}}

      数据: { thing: '华为手机', mood: '开心' }
      • 但是当情况复杂时,正则表达式的思路肯定就不行了。比如这样的模板字符串,
        是不能用正则表达式的思路实现的
        {{#arr}}
      • {{.}}
      • {{/arr}}

      mustache库的机理

      1.png

      什么是tokens

      • tokens是一个JS的嵌套数组,说白了,就是模板字符串的JS表示
      • 它是“抽象语法树”、“虚拟节点”等等的开山鼻祖
      • 其实tokens就是编译原理中第一步,解析中的词法分析结果
      模板字符串
      

      我买了一个{{thing}},好{{mood}}啊

      tokens [ ["text", "

      我买了一个"], ["name", "thing"], ["text", ",好"], ["name", "mood"], ["text", "啊

      "], ]

      循环情况下的tokens

        {{#arr}}
      • {{.}}
      • {{/arr}}
      [ ["text", "
        "], ["#", "arr", [ ["text", "
      • "], ["name", "."], ["text", "
      • "] ]], ["text", "
      "] ]

      双重循环情况下的tokens

      • 当循环是双重的,那么tokens会更深一层
          
        {{ #students}}
      1. 学生{{ name }}的爱好是
          {{ #hobbies}}
        1. {{.}}
        2. {{/ hobbies}}
      2. {{/ students}}
      [ ["text", "
        "], ["#", "students", [ ["text", "
      1. 学生"], ["name", "name"], ["text", "的爱好是
          "], ["#", "hobbies", [ ["text", "
        1. "], ["name", "."], ["text", "
        2. "], ]], ["text", "
      2. "], ]], ["text", "
      "] ]

      mustache库底层重点要做两个事情:

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

      实现简化mustache库

      • webpack.config.js
      const path=require('path');
      module.exports={
          //模式:开发
          mode:'development',
          //入口文件
          entry:'./src/index.js',
          //打包出口
          output:{
              filename:'bundle.js',
          },
          devServer:{
              //静态文件根目录
              contentBase:path.join(__dirname,"www"),
              //不压缩
              compress:false,
              //端口号
              port:8080,
              //虚拟打包的路径,bundle.js文件没有真正的生成
              publicPath:'/xuni/'
          }
      }
      
      • package.json
      {
        "name": "mustachedemo",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "dev": "webpack-dev-server"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "devDependencies": {
          "webpack": "^4.44.2",
          "webpack-cli": "^3.3.12",
          "webpack-dev-server": "^3.11.0"
        }
      }
      

      注意:如果想生成UMD模式的包。(这意味着它可以同时在nodejs环境中使用,也可以在浏览器环境中使用)。只需要一个“通用头”即可,百度就知道了。

      代码

      • 新建www文件夹&index.html
      
      
      
      
          
          
          Document
      
      
      
          
      新建src文件夹
      • index.js
      import parseTemplateToTokens from './parseTemplateToTokens.js';
      import renderTemplate from './renderTemplate.js';
      
      // 全局提供SSG_TemplateEngine对象
      window.SSG_TemplateEngine = {
          // 渲染方法
          render(templateStr, data) {
              // 调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
              var tokens = parseTemplateToTokens(templateStr);
              // 调用renderTemplate函数,让tokens数组变为dom字符串
              var domStr = renderTemplate(tokens, data);
              
              return domStr;
          }
      };
      
      • lookup.js
      //此处我的第一想法是递归,实际上不需要,很精妙
      
      /* 
          功能是可以在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];
      };
      
      • nestTokens.js
      /* 
          函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
      */
      export default function nestTokens(tokens) {
          // 结果数组
          var nestedTokens = [];
          // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
          var sections = [];
          // 收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组
          // 收集器的指向会变化,当遇见#的时候,收集器会指向这个token的下标为2的新数组
          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是谁,可能是结果nestedTokens,也可能是某个token的下标为2的数组,甭管是谁,推入collctor即可。
                      collector.push(token);
              }
          }
      
          return nestedTokens;
      };
      
      • parseArray.js
      import lookup from './lookup.js';
      import renderTemplate from './renderTemplate.js';
      
      /* 
          处理数组,结合renderTemplate实现递归
          注意,这个函数收的参数是token!而不是tokens!
          token是什么,就是一个简单的['#', 'students', [
      
          ]]
          
          这个函数要递归调用renderTemplate函数,调用多少次???
          千万别蒙圈!调用的次数由data决定
          比如data的形式是这样的:
          {
              students: [
                  { 'name': '小明', 'hobbies': ['游泳', '健身'] },
                  { 'name': '小红', 'hobbies': ['足球', '蓝球', '羽毛球'] },
                  { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] },
              ]
          };
          那么parseArray()函数就要递归调用renderTemplate函数3次,因为数组长度是3
      */
      
      export default function parseArray(token, data) {
          // 得到整体数据data中这个数组要使用的部分
          var v = lookup(data, token[1]);
          // 结果字符串
          var resultStr = '';
          // 遍历v数组,v一定是数组
          // 注意,下面这个循环可能是整个包中最难思考的一个循环
          // 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。
          for(let i = 0 ; i < v.length; i++) {
              // 这里要补一个“.”属性
              // 拼接
              resultStr += renderTemplate(token[2], {
                  ...v[i],
                  '.': v[i]
              });
          }
          return resultStr;
      };
      
      • parseTemplateToTokens.js
      import Scanner from './Scanner.js';
      import nestTokens from './nestTokens.js';
      
      /* 
          将模板字符串变为tokens数组
      */
      export default function parseTemplateToTokens(templateStr) {
          var tokens = [];
          // 创建扫描器
          var scanner = new Scanner(templateStr);
          var words;
          // 让扫描器工作
          while (!scanner.eos()) {
              // 收集开始标记出现之前的文字
              words = scanner.scanUtil('{{');
              if (words != '') {
                  // 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格
                  // 标签中的空格不能去掉,比如
      不能去掉class前面的空格 let isInJJH = false; // 空白字符串 var _words = ''; for (let i = 0; i < words.length; i++) { // 判断是否在标签里 if (words[i] == '<') { isInJJH = true; } else if (words[i] == '>') { isInJJH = false; } // 如果这项不是空格,拼接上 if (!/\s/.test(words[i])) { _words += words[i]; } else { // 如果这项是空格,只有当它在标签内的时候,才拼接上 if (isInJJH) { _words += ' '; } } } // 存起来,去掉空格 tokens.push(['text', _words]); } // 过双大括号 scanner.scan('{{'); // 收集开始标记出现之前的文字 words = scanner.scanUtil('}}'); if (words != '') { // 这个words就是{{}}中间的东西。判断一下首字符 if (words[0] == '#') { // 存起来,从下标为1的项开始存,因为下标为0的项是# tokens.push(['#', words.substring(1)]); } else if (words[0] == '/') { // 存起来,从下标为1的项开始存,因为下标为0的项是/ tokens.push(['/', words.substring(1)]); } else { // 存起来 tokens.push(['name', words]); } } // 过双大括号 scanner.scan('}}'); } // 返回折叠收集的tokens return nestTokens(tokens); }
      • renderTemplate.js
      import lookup from './lookup.js';
      import parseArray from './parseArray.js';
      /* 
          函数的功能是让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;
      }
      
      • Scanner.js
      /* 
          扫描器类
      */
      export default class Scanner {
          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.substring(this.pos);
              }
          }
      
          // 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
          scanUtil(stopTag) {
              // 记录一下执行本方法的时候pos的值
              const pos_backup = this.pos;
              // 当尾巴的开头不是stopTag的时候,就说明还没有扫描到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);
          }
      
          // 指针是否已经到头,返回布尔值。end of string
          eos() {
              return this.pos >= this.templateStr.length;
          }
      };
      

      你可能感兴趣的:(mustache模板引擎原理)