当前位置: 首页 > news >正文

Vue源码解析之mustache模板引擎

个人简介

👀个人主页: 前端杂货铺
🙋‍♂️学习方向: 主攻前端方向,正逐渐往全干发展
📃个人状态: 研发工程师,现效力于中国工业软件事业
🚀人生格言: 积跬步至千里,积小流成江海
🥇推荐学习:🍍前端面试宝典 🎨100个小功能 🍉Vue2 🍋Vue3 🍓Vue2/3项目实战 🥝Node.js实战 🍒Three.js

🌕个人推广:每篇文章最下方都有加入方式,旨在交流学习&资源分享,快加入进来吧

文章目录

    • 前言
    • 将数据变为视图
      • 纯DOM法
      • 数组的 join 方法
      • ES6 的反引号法
    • mustache 基本使用
    • mustache 核心理念
    • 手写 mustache
      • 搭建环境
      • 手撕 mustache
    • 总结

前言

知其然知其所以然,本篇文档我们来学习 Vue 源码之 mustache 模板引擎。

将数据变为视图

纯DOM法

纯 DOM 方法的关键点在于使用 createElement 创建节点和使用 appendChild 向节点的子节点列表的末尾添加新的子节点。

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Document</title>
</head>
<style>
  .hd {
    color: orange;
  }
  .bd {
    color: skyblue;
  }
</style>
<body>
<ul id="list">
</ul>
<script>
  // 定义人的基本信息
  const arr = [
    {"name": "小明", "age": 30, "gender": "male"},
    {"name": "小小", "age": 20, "gender": "female"},
    {"name": "小山", "age": 10, "gender": "male"},
  ]

  // 获取 ul
  const list = document.getElementById('list');

  // 循环人的基本信息数组,把信息构建至页面中
  for (let i = 0; i < arr.length; i++) {
    // 创建li标签
    let oLi = document.createElement("li");

    // 创建hd这个div,存放 “xx的基本信息”
    let hdDiv = document.createElement("div");
    hdDiv.className = 'hd';
    hdDiv.innerText = arr[i].name + '的基本信息';

    // 创建bd这个div,存放人的详细基本信息
    let bdDiv = document.createElement("div");
    bdDiv.className = 'bd';

    // 人的基本信息(姓名、年龄、性别)放入 p 标签中
    let p1 = document.createElement("p");
    p1.innerText = '姓名:' + arr[i].name;
    bdDiv.appendChild(p1);

    let p2 = document.createElement("p");
    p2.innerText = '年龄:' + arr[i].age;
    bdDiv.appendChild(p2);

    let p3 = document.createElement("p");
    p3.innerText = '性别:' + arr[i].gender;
    bdDiv.appendChild(p3);

    oLi.appendChild(hdDiv);
    hdDiv.appendChild(bdDiv);
    list.appendChild(oLi);
  }
</script>
</body>
</html>

在这里插入图片描述


数组的 join 方法

数组 join 方法关键在于字符串的拼接。

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Document</title>
</head>
<body>
<ul id="list">
</ul>
<script>
  // 定义人的基本信息
  const arr = [
    {"name": "小明", "age": 30, "gender": "male"},
    {"name": "小小", "age": 20, "gender": "female"},
    {"name": "小山", "age": 10, "gender": "male"},
  ]

  const list = document.getElementById('list');

  for (let i = 0; i < arr.length; i++) {
    list.innerHTML += [
      '<li>',
      '  <div class="hd"></div>' + arr[i].name + '的信息</div>',
      '  <div class="bd">',
      '    <p>姓名:' + arr[i].name + '</p>',
      '    <p>年龄:' + arr[i].age + '</p>',
      '    <p>性别:' + arr[i].gender + '</p>',
      '  </div>',
      '</li>'
    ].join('')
  }
</script>
</body>
</html>

在这里插入图片描述


ES6 的反引号法

ES6 反引号法可以更优雅的把 html 标签写入字符串中。

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Document</title>
</head>
<body>
<ul id="list"></ul>
</body>
<script>
  // 定义人的基本信息
  const arr = [
    {"name": "小明", "age": 30, "gender": "male"},
    {"name": "小小", "age": 20, "gender": "female"},
    {"name": "小山", "age": 10, "gender": "male"},
  ]

  const list = document.getElementById('list');

  for (let i = 0; i < arr.length; i++) {
    list.innerText += `
      <li>
        <div class="hd">${arr[i].name}的基本信息</div>
        <div class="bd">
          <p>姓名:${arr[i].name}</p>
          <p>性别:${arr[i].gender}</p>
          <p>年龄:${arr[i].age}</p>
        </div>
      </li>
    `
  }
</script>
</html>

在这里插入图片描述


mustache 基本使用

mustache.js 是一个简单强大的 JavaScript 模板引擎,使用它可以简化在 js 代码中的 html 编写。

基本使用

// 初始化
npm	init -y
// 安装 mastache
npm install mastache

mustache 的使用非常简单, 使用花括号 {{#xxx}} 开始,使用 花括号 {{/xxx}} 结束,中间包裹着 key 值,最后再使用 mustache.render(templateStr, data),完成数据的渲染。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Document</title>
  </head>
  <body>
    <div id="container"></div>

    <!-- 模版 -->
    <script type="text/template" id="myTemplate">
      <ul>
        {{#arr}}
          <li>
            <div class="hd">{{name}}的基本信息</div>
            <div class="bd">
              <p>姓名:{{name}}</p>
              <p>性别:{{gender}}</p>
              <p>年龄:{{age}}</p>
            </div>
          </li>
        {{/arr}}
      </ul>
    </script>

    <script type="module">
      import mustache from "./node_modules/mustache/mustache.mjs";
	  // 获取模版
      const templateStr = document.getElementById("myTemplate").innerHTML;
	  // 定义数据
      const data = {
        arr: [
          { name: "小明", age: 30, gender: "male" },
          { name: "小小", age: 20, gender: "female" },
          { name: "小山", age: 10, gender: "male" },
        ],
      };
      const domStr = mustache.render(templateStr, data);
      const container = document.getElementById("container");
      container.innerHTML = domStr;
    </script>
  </body>
</html>

在这里插入图片描述


mustache 核心理念

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

对于一维模板字符串及对应 tokens,如下:

在这里插入图片描述

对于二维模板字符串及 tokens 如下:

在这里插入图片描述

mustache 库底层重点要做的两件事情

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

在这里插入图片描述


手写 mustache

搭建环境

总体目录如下:

在这里插入图片描述

新建一个文件夹,打开终端,输入以下命令后回车以初始化项目。

npm init -y

安装依赖。

npm install

安装 webpack、webpack-dev-server、webpck-cli。

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

修改 package.json 文件的 dev 内容如下,更换项目启动方式。

{
  "name": "templateengine",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "webpack": "^4.47.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.3"
  }
}

手动创建 webpack.config.js 文件,并进行如下配置。

const path = require("path");

module.exports = {
  // 模式,开发
  mode: "development",
  // 入口
  entry: "./src/index.js",
  // 打包到什么文件
  output: {
    filename: "bundle.js",
  },
  // 配置一下 webpack-dev-server
  devServer: {
    // 静态文件根目录
    contentBase: path.join(__dirname, "www"),
    // 压缩
    compress: false,
    // 端口号
    port: 8080,
    // 虚拟打包到路径,bundle.js文件没有真正的生成
    publicPath: "/xuni/",
  },
};

手动创建 www 文件及 www/index.html 文件,添加如下内容。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>你好,这里是前端杂货铺!</h1>
    <script src="/xuni/bundle.js"></script>
  </body>
</html>

手动创建 src 文件及 src/index.js 文件,添加如下内容。

console.log('hello');

OK,此时基本配置已完成,接下来我们打开终端键入 npm run dev 命令回车,然后访问 8080 端口。

在这里插入图片描述


手撕 mustache

手撕 mustache 源码地址

接下来,我们来手写简易版的 mustache,体验一下模版引擎的设计巧妙之处。

代码文件结构如下:

在这里插入图片描述

index.html 文件中,我们编写 模版字符串数据,并使用 TemplateEngine.render(模板字符串, 数据) 来获取 dom 数据,之后添加到 div 容器中。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="container"></div>
    <script src="/xuni/bundle.js"></script>
    <script>
      // 模板字符串
      const templateStr = `
        <div>
          <ul>
            {{#students}}
            <li>
              学生{{name}}的爱好是
              <ol>
                {{#hobbies}}
                <li>{{.}}</li>
                {{/hobbies}}
              </ol>
            </li>
            {{/students}}
          </ul>
        </div>
      `;
      // 数据
      const data = {
        students: [
          {
            name: "张三",
            hobbies: ["敲代码", "乒乓球", "健身"],
          },
          {
            name: "李四",
            hobbies: ["游泳", "跑步"],
          },
          {
            name: "王五",
            hobbies: ["唱歌", "跳舞"],
          },
        ],
      };
      // 获取 dom 数据
      const domStr = TemplateEngine.render(templateStr, data);
      const container = document.getElementById("container");
      container.innerHTML = domStr;
    </script>
  </body>
</html>

src/index.js 文件中,全局提供模板引擎对象,渲染方法的入参为定义的 模板字符串数据,通过 parseTemplateToTokens 解析字符串为 tokens,通过 renderTemplate 渲染 tokens 数组为 dom 字符串。

import parseTemplateToTokens from "./parseTemplateToTokens";
import renderTemplate from "./renderTemplate";
// 全局提供模版引擎对象
window.TemplateEngine = {
  // 渲染方法
  render(templateStr, data) {
    // 模版字符串变为tokens数组
    const tokens = parseTemplateToTokens(templateStr);
    // 调用renderTemplate方法,渲染tokens数组为dom字符串
    const domStr = renderTemplate(tokens, data);
    // 返回组织好的 dom 字符串
    return domStr;
  },
};

通过上面两个核心方法,我们将获得如下的数据。

在这里插入图片描述

在这里插入图片描述

扫描器类 Scanner,用于扫描传入的模板字符串。当遇到 {{}} 内容时停止扫描,并返回停止扫描之前路过的内容。

/**
 * 扫描器类
 */
export default class Scanner {
  constructor(templateStr) {
    this.templateStr = templateStr;
    // 指针
    this.pos = 0;
    // 尾巴,初始值为传入的字符串
    this.tail = templateStr;
  }

  // 走过指定的内容,没有返回值,用于过滤掉 {{ 和 }} 这样的内容
  scan(tag) {
    // 如果捕获到了tag,就让指针后移tag的长度,不会做任何处理,只是过滤掉
    if (this.tail.indexOf(tag) === 0) {
      // {{ 和 }} 的长度为2,就让指针后移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);
    }

    // 返回扫描过的内容(不包括stopTag)
    return this.templateStr.substring(pos_backup, this.pos);
  }

  // 指针是否已经到头
  eos() {
    return this.pos >= this.templateStr.length;
  }
}

parseTemplateToTokens.js 文件用于将模板字符串变为初步的 tokens 数组。在 Scanner 的加持下,按照一定的规则 “制作” tokens。

import Scanner from "./Scanner";
import nestTokens from "./nestTokens";
/**
 * 将模板字符串变为tokens数组
 */
export default function parseTemplateToTokens(templateStr) {
  // 存储 tokens
  const tokens = [];
  // 创建扫描器,扫描模板字符串
  const scanner = new Scanner(templateStr);
  let words;
  // 让扫描器工作(当扫描器的指针没有到头时)
  while (!scanner.eos()) {
    // 扫描字符,直到遇到 {{ 时结束,因为 {{ 之前的内容是text类型
    words = scanner.scanUtil("{{");

    // 如果扫描的内容不为空,就作为text类型存入tokens
    if (words !== "") {
      tokens.push(["text", words]);
    }

    // 过滤 {{,指针后移2位,因为 {{ 并没有实质作用,过滤掉就好
    scanner.scan("{{");

    // 再次扫描字符,直到遇到 }} 时结束,因为 {{ 和 }} 之间的内容是需要处理的name类型
    words = scanner.scanUtil("}}");

    // 根据再次扫描的内容的第一个字符,判断应该 push 到 tokens 中的类型
    if (words !== "") {
      // 按照第一个字符组织tokens,# 为遍历的开始,/ 为遍历的结束
      if (words[0] === "#") {
        tokens.push(["#", words.substring(1)]);
      } else if (words[0] === "/") {
        tokens.push(["/", words.substring(1)]);
      } else {
        tokens.push(["name", words]);
      }
    }
    scanner.scan("}}");
  }
  // 返回折叠收集的tokens
  return nestTokens(tokens);
}

在这里插入图片描述

要进行进一步的 tokens 处理(多层级),就需要 nestTokens.js 来大显身手了。

/**
 * 折叠tokens,将#和/之间的tokens能够整合起来,作为它下标为2的项
 */
export default function nestTokens(tokens) {
  // 结果数组,需要最后返回的
  const nestedTokens = [];
  // 栈结构,用于保存 # 的token
  const sections = [];
  // 收集器引用,默认指向nestedTokens
  let collector = nestedTokens;

  // 遍历在 parseTemplateToTokens 中组织好的 tokens
  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i];
    switch (token[0]) {
      // 第一个字符如果为 #,就往收集器中放入这个token,并且入栈
      case "#":
        // 收集器中放入这个token
        collector.push(token);
        // 入栈
        sections.push(token);
        // 收集器此时要换位这个token的下标为2的项,因为之后需要push的是它的子项
        collector = token[2] = [];
        break;
      case "/":
        // 出栈
        sections.pop();
        // 改变收集器为栈结构末尾的数组
        collector =
          sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
        break;
      default:
        // 收集器中放入这个token
        collector.push(token);
    }
  }

  return nestedTokens;
}

在这里插入图片描述

下面我们要做的就是把最终的 tokens 转为 dom 字符串了。

编写 lookUp.js 文件,用于在对象中查找 key 值。如 dataObj = {a: {b: {c: 100}}},keyName = 'a.b.c',那么调用该方法将返回100。

/**
 * 在 dataObj 对象中查找 keyName 的值
 * 如dataObj = {a: {b: {c: 100}}},keyName = 'a.b.c',返回100
 * @param {*} dataObj 对象
 * @param {*} keyName 要查找的key
 * @returns
 */
export default function lookUp(dataObj, keyName) {
  // 查看 keyName 中是否有 ., 但不能是 .
  if (keyName !== "." && keyName.indexOf(".") !== -1) {
    // 以 . 分隔,例如 a.b.c,先拆分为 [a, b, c]
    const keys = keyName.split(".");
    // 临时变量,用于逐层查找
    let temp = dataObj;
    // 每找一层,就把它设置为新的临时变量
    for (let i = 0; i < keys.length; i++) {
      temp = temp[keys[i]];
    }
    return temp;
  }

  // 没有.直接返回
  return dataObj[keyName];
}

下面我们编写 parseArray.jsrenderTemplate.js 文件,分别用于处理数组和渲染模板字符串。

import lookUp from "./lookUp";
import renderTemplate from "./renderTemplate";
/**
 * 处理数组,结合 renderTemplate 方法实现递归
 * token:["#", "students", [xxx]]
 *
 * 这个函数要递归调用 renderTemplate 方法
 * 调用次数取决于 data 中的数组长度
 */
export default function parseArray(token, data) {
  // 得到数据整体data中这个数组要使用的数据
  const v = lookUp(data, token[1]);
  // 结果字符串
  let resultStr = "";
  // 遍历数据,而不是tokens。数组中的数据有几条就遍历几次
  for (let i = 0; i < v.length; i++) {
    // 拼接,在此处要补一个 “.” 属性
    resultStr += renderTemplate(token[2], {
      ...v[i], // 简单数组,直接展开
      ".": v[i], // 为了处理 {{.}} 的情况
    });
  }

  return resultStr;
}
import lookUp from "./lookUp";
import parseArray from "./parseArray";

/**
 * 渲染模版
 * @param {*} tokens
 * @param {*} data
 * @returns
 */
export default function renderTemplate(tokens, data) {
  let resultStr = "";
  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i];
    if (token[0] === "text") {
      resultStr += token[1];
    } else if (token[0] === "name") {
      // name类型,获取值
      resultStr += lookUp(data, token[1]);
    } else if (token[0] === "#") {
      // 递归处理下标为2·的数组
      resultStr += parseArray(token, data);
    }
  }
  return resultStr;
}

在这里插入图片描述

此时编码完毕,打开浏览器查看渲染内容如下,至此,手撕完毕!!

在这里插入图片描述


总结

本篇文章,我们首先认识了几种将模型变为视图的方法。

之后学习了Mustache 的基本使用和核心理念。

通过手写 Mustache 模板引擎,我们 深入理解 了其 工作原理和实现细节。虽然手写版本可能不如官方实现那么完善,但 这个过程让我对模板引擎的设计有了更深刻的认识。Mustache 的简洁性和无逻辑特性使其成为一种非常灵活的模板引擎,适用于各种场景。

如果你也对模板引擎的实现感兴趣,我建议你尝试手写一个简化版的 Mustache 或类似的模板引擎。这不仅能够加深你对前端技术的理解,还能提升你的编程能力。

好啦,本篇文章到这里就要和大家说再见啦,祝你这篇文章阅读愉快,你下篇文章的阅读愉快留着我下篇文章再祝!


参考资料:

  1. mustache 百度百科
  2. Vue源码解析之mustache模板引擎(尚硅谷)

在这里插入图片描述


相关文章:

  • nodejs express设置允许跨域示例
  • C#运算符详解
  • 【免费】2013-2019年上市公司知识产权数据
  • 【架构艺术】Go语言微服务monorepo的代码架构设计
  • C、C++读取空格、回车符函数【getline、cin.get、cin.getline、std::noskipws】
  • 仿muduo库实现高并发服务器-面试常见问题
  • C#核心(22)string
  • 从0开始完成基于异步服务器的boost搜索引擎
  • 可重构智能表面(RIS)的全面介绍
  • 渐进稀疏注意力PSA详解及代码复现
  • KMP 算法的 C 语言实现
  • ROS2-话题学习
  • RabbitMQ高级特性--消息确认机制
  • [网络爬虫] 动态网页抓取 — Selenium 入门操作
  • 搞定python之一----开发环境配置
  • AtCoder Beginner Contest 396(ABCDEF)
  • 【LLM】大模型推理、微调显卡挑选一览表
  • 【论文解读】《LIMO: Less is More for Reasoning》
  • PHP的Workerman 和 Java 常驻内存的相似性
  • Java【网络原理】(3)网络编程续
  • 58同城类型网站制作/放心网站推广优化咨询
  • 重庆高端网站seo/视频号排名优化帝搜软件
  • 企业平台网站建设/推广app最快的方法
  • 网站效果展示/关键词难易度分析
  • 泰国一家做男模的网站/深圳网站制作
  • 魅力网络营销公司/网站搜索优化技巧