【前端源码解析】mustache 模板引擎核心原理

参考视频:Vue 源码解析系列课程

系列笔记:

大纲:

  • 什么是模板引擎
  • mustache 基本使用
  • mustache 的底层核心机理
  • 带你手写 mustache 库

本章源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/SGG_TemplateEngine

数据 -> 视图

数据变为视图的方法:

  • 纯 DOM 法:非常笨拙,没有实战价值
  • 数组 join 法:曾经非常流行
  • ES6 的反引号:ES6 的语法糖,很好用
  • 模板引擎:最优雅的解决方案

数据准备:

var arr = [
	{ "name": "小明", "age": 12, "sex": "男" },
	{ "name": "小红", "age": 11, "sex": "女" },
	{ "name": "小强", "age": 13, "sex": "男" }
];

纯 DOM 法:十分麻烦,了解一下即可

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

for (var i = 0; i < arr.length; i++) {
	// 每遍历一项,都要用DOM方法去创建li标签
	let oLi = document.createElement('li');
	// 创建hd这个div
	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].sex;
	bdDiv.appendChild(p3);

	// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
	oLi.appendChild(hdDiv);
	// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
	oLi.appendChild(bdDiv);
	// 创建的节点是孤儿节点,所以必须要上树才能被用户看见
	list.appendChild(oLi);
}

数组 join 法:第一个想到的人很厉害,强行让 for 里面的代码具有层次可读性

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

// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for (let i = 0; i < arr.length; i++) {
	list.innerHTML += [
		'<li>',
		'    <div class="hd">' + arr[i].name + '的信息</div>',
		'    <div class="bd">',
		'        <p>姓名:' + arr[i].name + '</p>',
		'        <p>年龄:' + arr[i].age  + '</p>',
		'        <p>性别:' + arr[i].sex + '</p>',
		'    </div>',
		'</li>'
	].join('')
}

ES6 的反引号:JavaScript 自身也在快速的发展

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

// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for (let i = 0; i < arr.length; i++) {
	list.innerHTML += `
		<li>
			<div class="hd">${arr[i].name}的基本信息</div>    
			<div class="bd">
				<p>姓名:${arr[i].name}</p>    
				<p>性别:${arr[i].sex}</p>    
				<p>年龄:${arr[i].age}</p>    
			</div>    
		</li>
	`;
}

最优雅的解决方案:模板引擎。

mustache 库

mustache 官方 git:https://github.com/janl/mustache.js

mustache 是“胡子”的意思,因为它的嵌入标记 {{ }} 非常像胡子

mustache 是最早的模板引擎库,比 Vue 诞生早很多。它的底层实现机理在当时非常有创造性、轰动性、为后续模板引擎的发展提供了崭新的思路。

BootCDN - Bootstrap 中文网开源项目免费 CDN 加速服务

基本使用

循环对象数组:

不循环,渲染数据对象:

循环简单数组:

数组可嵌套:

布尔值控制是否渲染:

底层核心机理

正则表达式实现?

比较简单的情况下,是可以用正则表达式实现的。

js 中字符串的 replace() 的第二个参数,可以是一个函数,其中 $x 代表第 x 个子表达式匹配到的文本
参考:JavaScript replace() 方法

// 模板字符串
var templateStr = '<h1>我买了一个{{thing}},花了{{money}}元,好{{mood}}</h1>';

// 数据
var data = {
	thing: '白菜',
	money: 5,
	mood: '激动'
};

// 渲染函数
function render(templateStr, data) {
	// 该例中只有一个子表达式,使用 $1 可以分别捕获到 'thing', 'money', 'mood'
	return templateStr.replace(/\{\{(\w+)\}\}/g, function(findStr, $1) {
		return data[$1];
	})
}

console.log(render(templateStr, data))
// <h1>我买了一个白菜,花了5元,好激动</h1>

当情况复杂时,无法使用正则表达式的思路来实现,比如以下模板字符串:

<div>
	<ul>
		{{#arr}}
		<li>{{.}}</li>
		{{/arr}}
	</ul>
</div>
原理 - tokens

mustache 库的机理:

mustache 库底层重点做的两件事:

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

tokens 是什么?

  • 一个 JS 的嵌套数组,即 模板字符串的 JS 表示
  • 它是 “抽象语法树”、“虚拟节点” 等的开山鼻祖

对于以下模板字符串:

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

生成的 tokens:注意 tokens 解析的是字符串,所以 html 标签也是当字符串对待的

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

模板解析原理:

循环情况下的 tokens:

当循环是双重的,那么 tokens 会更深一层:

修改源码来观察 tokens:在源码中进行以下修改,即可在控制台输出 tokens

return nestTokens(squashTokens(tokens));
var tokens = nestTokens(squashTokens(tokens));
console.log(tokens);
return tokens;

手写实现

本章源码:https://gitee.com/szluyu99/vue-source-learn/tree/master/SGG_TemplateEngine

webpack 和 webpack-dev-server 构建

模块化打包工具有 webpack (webpack-dev-server)、rollup、Parcel 等

Mustache 官方使用 rollup 进行模块化打包,我们使用 webpack (webpack-dev-server) 进行模块化打包

  • webpack 开发体验很好,有热更新功能,可以在浏览器中实时调试程序
  • 相比 nodejs,浏览器的控制台更好用(可以点击展开数组等)
  • 生成库是 UMD 的,这意味着它可以同时在 nodejs 环境中使用,也可以在浏览器环境中使用。

实现 UMD 不难,只需要一个 “通用头” 即可?
参考:前端模块化发展(CommonJs、AMD、CMD、UMD、ESM)

# 构建项目,一路回车
npm init

# 注意版本可能会有兼容问题,最好使用以下版本
npm install -D webpack@4
npm install -D webpack-dev-server@3
npm i -D webpack-cli@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/"
    }
};

源码

源码学习注意事项

  • 学习源码时,源码思想要借鉴,而不要抄袭。要能够发现源码中书写的精彩的地方
  • 将独立的功能拆写为独立的 js 文件中完成,通常是一个独立的类,每个单独的 功能必须能独立的 “单元测试”
  • 应该围绕中心功能,先把主干完成,然后修剪枝叶
  • 功能并不需要一步到位,功能的拓展要一步步完成,有的非核心功能甚至不需实现

整体架构::

使用效果:

var templateStr = `
<ul>
	{{#arr}}
		<li>
			<div class="hd">{{name}}的基本信息</div>
			<div class="bd">
				<p>姓名:{{name}}</p>
				<p>性别:{{sex}}</p>
				<p>年龄:{{age}}</p>
			</div>
		</li>
	{{/arr}}
</ul>
`
var data = {
	arr: [
		{ "name": "小明", "age": 12, "sex": "男" },
		{ "name": "小红", "age": 11, "sex": "女" },
		{ "name": "小强", "age": 13, "sex": "男" },
	]
}

var domStr = SGG_TemplateEngine.render(templateStr, data);

var container = document.getElementById('container')
container.innerHTML = domStr
scan 与 scanUntil

源码:https://gitee.com/szluyu99/vue-source-learn/blob/master/SGG_TemplateEngine/src/Scanner.js

Scanner 类中的两个方法:

  • scan 就是走过指定内容,没有返回值
  • scanUntil 让指针进行扫描,直到遇到指定内容结束,并且能否返回结束之前路过的文字

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

  • 使用 scanUntil('{{') 会返回 我买了一个
  • 使用 scan('{{') 会跳过 {{
  • 然后使用 scanUntil('}}') 会返回 thing
  • 使用 scan('}}') 会跳过 }}
  • 重复以上操作

parseTemplateToTokens

源码:https://gitee.com/szluyu99/vue-source-learn/blob/master/SGG_TemplateEngine/src/parseTemplateToTokens.js

parseTemplateToTokens 用于将模板字符串变为 tokens 数组

简单情况:

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

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

复杂情况:

相比简单情况,复杂情况中有零散的 tokens 嵌套起来

renderTemplate

源码:https://gitee.com/szluyu99/vue-source-learn/blob/master/SGG_TemplateEngine/src/renderTemplate.js

renderTemplate 用于将 tokens 数组变为 dom 字符串(结合数据)

简单情况:

var templateStr = `我买了一个{{thing}},好{{mood}}啊,我考了{{my.score}}分!`;
var data = {
	thing: '苹果',
	mood: '开心',
	my : {
		score: 100
	}
};

复杂情况:

var templateStr = `
	<div>
		<ul>
			{{#students}}
			<li class="myli">
				学生{{name}}的爱好是
				<ol>
					{{#hobbies}}
					<li>{{.}}</li>
					{{/hobbies}}
				</ol>
			</li>
			{{/students}}
		</ul>
	</div>
`;
var data = {
	students: [
		{ 'name': '小明', 'hobbies': ['编程', '游泳'] },
		{ 'name': '小红', 'hobbies': ['看书', '弹琴', '画画'] },
		{ 'name': '小强', 'hobbies': ['锻炼'] }
	]
};
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌宅鹿同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值