前言
第一次使用掘金写文章,今天来分享一下手写mustache这个编译的模板引擎!!!那么接下来就是来说说这个是如何手写的呢?
详细过程
render()函数
首先呢 ,我们得有一个render函数 来帮助我们编译模板,那么这个render函数该如何写呢?
import templateTokens from './tokens.js';
import templateRener from './templateRener.js'
window.gx_mustache = {
//模板字符串转换tokens数组 => tokens数组变为dom字符串
render(templateStr, data) {
//将模板字符串转换tokens数组
let tokens = templateTokens(templateStr);
// 让tokens数组变为dom字符串
let domStr = templateRener(tokens, data);
//返回
return domStr;
}
};
然后可以通过调用来生成我们的模板字符串咯
gx_mustache.render(templateStr, data); //templateStr为模板字符串 data为数据
//然后就可以挂载到contain容器咯
var contain = document.getElementById("contain");
contain.innerHTML = domStr;
上面templateTokens 、templateRener又是什么呢? 这里说一下
templateTokens() //这个是将模板字符串转换tokens数组
templateRener() //这个是将tokens数组变为dom字符串
templateTokens() 的结果
templateRener() 的结果
那么这俩个函数又是怎样的呢? 首先来看
templateTokens() 函数
export default function templateTokens(templateStr) {
var tokens = [];
// 创建扫描器
var scanner = new Scanner(templateStr);
var words;
// 让扫描器工作
while (!scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUtil('{{');
if (words != '') {
// 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格
// 标签中的空格不能去掉,比如<div class="box">不能去掉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);
}
这里使用了一个扫描类Scanner来帮助我们找到这些模板字符串
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;
//尾巴也要变
this.tail = this.templateStr.substring(this.pos);
}
}
//扫描
// stopTag停止标记
scanUtil(stopTag) {
//记录一下pos的值,一开始不一定是0
const back_pos = this.pos;
while (this.tail.indexOf(stopTag) != 0 && !this.eos()) {
this.pos++;
//后边是它的尾巴
this.tail = this.templateStr.substring(this.pos);
}
// 循环返回扫描的内容
return this.templateStr.substring(back_pos, this.pos)
}
//指针是否到头
eos(){
return this.pos >= this.templateStr.length
}
}
另外还使用了nestTokens函数来折叠数组
*nestTokens()函数
/*
函数的功能是折叠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];
// console.log(token);
//token[0] 指的是# / text name这些
//token[1] 指的的word
//token[2] 指的是有子级
switch (token[0]) {
case '#':
// 收集器中放入这个token
collector.push(token);
// console.log(collector);
// 入栈
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;
};
那么templateTokens函数就这样完成咯。
下面来看看另一个函数templateRener() 这个是将tokens数组变为dom字符串
templateRener()函数
import lookup from './lookup';
import parseArray from './parseArray'
// 让tokens数组变为dom字符串
export default function templateRener(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;
}
这个函数也借助了俩个函数lookup()和parseArray()来完成的
下面来看看lookup()函数
export default function lookup(obj, keyName) {
//首先判断keyName是否含有.
if (keyName.indexOf('.') != -1 && keyName != '.') {
//有就拆分
let keys = keyName.split(".");
let temp = obj;
for (let i; i < keys.length; i++) {
temp = temp[keys[i]];
}
//循环结束后返回temp
return temp;
}
//没有. 就直接使用
return obj[keyName];
}
然后就是parseArray()函数
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;
};
最后 一个简易版的mustache模板引擎就这样做好咯 下面来测试一下
// 模板
var templateStr = `
<div>
<ul>
{{#students}}
<li>
学生{{name}}的爱好是
<ol>
{{#hobbies}}
<li class = "xixi">{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/students}}
</ul>
</div>
`;
// 数据
var data = {
students: [
{ 'name': '故心', 'hobbies': ['编程', '游泳'] },
{ 'name': '故心心', 'hobbies': ['看书', '弹琴', '画画'] },
{ 'name': '故心要加油', 'hobbies': ['锻炼'] }
]
};
// 调用render方法
var domStr = gx_mustache.render(templateStr, data);
// console.log(domStr);
//挂载到contain容器汇总
var contain = document.getElementById("contain");
contain.innerHTML = domStr;
结果
哈 ,完成啦,给自己一个赞,继续加油。