手写Mustache
文章目录
mustache
基本使用
mustache.js 是 mustache 模板系统的JavaScript实现。Mustache 是一套轻逻辑的模板语法。它可以用来处理 HTML 、配置文件、源代码等任何文件。它把模板中的标签展开成给定的数据映射或者对象中的属性值。我们之所以说“轻逻辑”,是因为模板里面没有if语句、else语句或者for循环。只有模板标签。
使用简介 使用方法
github地址:mustache.js
下面是一个示例:
<script id="template" type="x-tmpl-mustache">
<div>
{{#name}}
<span>is {{age}} years old</span>
{{/name}}
</div>
</script>
const view = {
name: 'Zuckjet',
gender: 25
};
const html = document.getElementById('template').innerHTML;
const output = Mustache.render(html, view);
console.log(output);
结果
<div>
<span>Zuckjet is 25 years old</span>
</div>
我们可以看到,通过使用Mustache.render方法就能把给定的模板转变为我们需要的内容,那么mustache.js是如何实现这些功能的呢?
Writer.prototype.render = function render (template, view, partials, tags) {
var tokens = this.parse(template, tags);
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template, tags);
};
通过上面的代码,我们可以很清楚地知道mustache.js渲染模板主要分为两大步骤:
将模板转换为tokens数组
将tokens数组转换为对应的html
Mustache.render方法返回的结果就是我们需要的内容字符串,这一点没有什么疑问。但是对于tokens数组是什么样子我们可能会比较好奇,现在我们来看看基本示例中生成的tokens数组:
tokens = [
["text", "↵ <div>↵", 0, 11],
["#", "name", 17, 26, Array(5), 82],
["text", " </div>↵ ", 92, 105]
]
//其中元素tokens[1]数组的内容为:
tokens[1] = [
["text", " <span>", 27, 39],
["name", "name", 39, 47],
["text", " is ", 47, 51],
["name", "age", 51, 58],
["text", " years old</span>↵", 58, 76]
]
不难发现tokens数组每一项都是形如:
[ type, value, start, end ]
这种形式,分别表示节点类型、节点值、节点匹配的起始位置和结束位置。
现在我们继续看看mustache.js
是如何生成这些tokens数组的。第一步生成tokens数组
中使用的this.parse(template, tags);
方法内部主要调用的是parseTemplate (template, tags)
函数:
function parseTemplate(template, tags){
// 列出该函数内部几个重要的方法,其余代码省略
scanner.scanUntil(regx);
scanner.scan(regx);
squashTokens(tokens);
nestTokens(squashedTokens);
}
Scanner
类是mustache.js
中一个核心的工具类,主要功能是根据传入的正则表达式切割字符串。其中scanner.scanUntil(regx)
是把符合正则表达式之前的字符串截取出来,sanner.scan(regx)
是把符合正则表达式内容截取出来。我们看看这两个函数的实现,就容易发现区别了:
Scanner.prototype.scanUntil = function scanUntil (re) {
var index = this.tail.search(re), match; //找到开始匹配正则的索引
switch (index) {
case -1:
match = this.tail;
this.tail = '';
break;
case 0:
match = '';
break;
default:
match = this.tail.substring(0, index); //截取匹配索引之前的内容
this.tail = this.tail.substring(index);
}
this.pos += match.length;
return match;
};
Scanner.prototype.scan = function scan (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0)
return '';
var string = match[0];
this.tail = this.tail.substring(string.length);
this.pos += string.length;
return string;
};
接下来我们看看squashTokens这个函数,squash这个单词我们并不陌生,git里面可以用这个命令合并多个commit记录。因此squashTokens的作用也不难猜到,主要用来合并text token。
tokens = [
["text","<", 5, 6]
["text","d", 6, 7]
["text","i", 7, 8]
["text","v", 8, 9]
["text",">", 9, 10]
];
squashTokens = [["text","<div>", 5, 10]];
nestTokens的作用则是把squashTokens数组转化成层级嵌套的树结构,也就是本章节开头列出的tokens数组结构。
至此如何生成tokens数组我们已经弄清楚了,接下来的任务主要是了解如何由tokens数组生成我们需要的html内容。这一步的调用的this.renderTokens方法组成代码如下:
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) {
var buffer = '';
var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
value = undefined;
token = tokens[i];
symbol = token[0];
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
else if (symbol === '>') value = this.renderPartial(token, context, partials, tags);
else if (symbol === '&') value = this.unescapedValue(token, context);
else if (symbol === 'name') value = this.escapedValue(token, context);
else if (symbol === 'text') value = this.rawValue(token);
if (value !== undefined)
buffer += value;
}
return buffer;
};
这个方法里重点是几个if else循环的处理,根据不同的token类型进行不同的处理。比如symbol等于text的时候,直接取出字符串值。symbol等于#的时候,会判断值的真假,为假时就不返回空。最后把各个类型的token处理的结果拼接起来,就是我们需要的html内容了。
我们知道,简单的模板引擎可以使用正则等字符串处理手段将模板字符串拼接为js源码字符串,然后使用eval或者new Function()手段执行拼接后的源码。这种方式实现模板引擎缺陷也很明显,不论是eval还是new Function()执行拼接后的js代码,其效率都很低下,其次动态执行字符串对于调试也很不友好。
mustache.js相当于是实现了自己的一套语法逻辑。通过构造tokens数组树,然后根据tokens数组树解析成目标字符串。不过自己实现语法规则,肯定无法实现js全部语言特性,因此mustache.js只能通过自己的语法约定,通过特定方式实现少数的js语法特性。
自己实现
目录结构
扫描类Scanner
扫描字符串,判断字符串“{{”、“}}” 进行步进计算,存储走过的字符串
/*
* @pram 扫描器类
*
*
* */
export default class Scanner{
constructor(templateStr) {
console.log(templateStr)
// 指针
this.pos = 0;
// 尾巴 ,一开始就是模板字符串的原文
this.tail = templateStr;
this.templateStr = templateStr;
}
// 入过指定内容
scan(tag){
if(this.tail.indexOf(tag) === 0){
//tag有多长,比如“{{” 长度是2 ,就让指针后移多少位
this.pos += tag.length;
//尾巴移动
this.tail = this.templateStr.substring(this.pos);
}
}
// 让指针进行扫描 ,直到遇见指定内容结束,并且返回结束之前入过的文字
scanUtil(stopStag){
//记录一下执行本方法的时候的pos值
const pos_backup = this.pos;
//当尾巴的开头不是stopTag的时候,就说明还没扫描到stopTag
while( !this.eos() &&this.tail.indexOf(stopStag) !== 0){
//指针向后移
this.pos++;
//改变尾巴为从当前指针这个字符串开始,到最后的全部字符
this.tail = this.templateStr.substring(this.pos);
}
return this.templateStr.substring(pos_backup,this.pos)
}
//指针是否到头, 返回布尔值
eos(){
return this.pos >= this.templateStr.length
}
}
将模板字符串变成初步tokens
- 对“{{” 、“}}”、“#” 、“/” 进行处理
parseTemplateToTokens函数
import Scanner from "./Scanner";
import nestTokens from "./nestTokens";
/*
* 将模板字符串变成tokens
*
* */
export default function parseTemplateToTokens(templateStr) {
let tokens = [];
//实例化 扫描器 ,构造时候提供一个参数(模板字符串)
// 这个扫描器就是针对字符串工作
let scanner = new Scanner(templateStr)
let words ;
//当指针没有到头
while (!scanner.eos()){
// 收集开始标记之前出现的文字
words = scanner.scanUtil('{{')
// 将收集的文字存起来 ,添加text
words.length && tokens.push(['text',words]);
// 過双大括号
scanner.scan("{{")
// 收集{{}} 里面的内容
words = scanner.scanUtil('}}')
//存起{{}} 里面的内容添加
if (words.length){
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);
}
将token生成嵌套tokens
方法nestTokens
官方nestTokens
/**
* Forms the given array of `tokens` into a nested tree structure where
* tokens that represent a section have two additional items: 1) an array of
* all tokens that appear in that section and 2) the index in the original
* template that represents the end of that section.
*/
function nestTokens (tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
case '^':
collector.push(token);
sections.push(token);
collector = token[4] = [];
break;
case '/':
section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
自己实现tokens
/*
* - 函数功能折叠tokens
* 将 “ # ”和“ / " 之间的tokens 能够整合在一起 ,作为下标为3 的项
*
* */
export default function nestTokens(tokens){
//结果数组
let nestTokens = []
//栈结构 ,存放小的tokens 栈(后进先出)
let tokenStack = [] ;
//收集器,一开始指向原数组, 引用类型值,指向同一个数组
// 收集器会变化, 如果遇到 “ # ” 的时候,收集器会指向这token下标为2的新数组
let collector = nestTokens;
for (let key in tokens ){
let token = tokens[key];
switch (token[0]) {
case '#':
// 收集器中存放这个token
collector.push(token);
//入栈
tokenStack.push(token);
//收集器更换, 给token 添加下标为2的项, 并且让收集器指向它
collector = token[2] = [] ;
break;
case '/':
//出栈
tokenStack.pop();
//改变收集器 为 栈结构的栈顶 那项下标为2的数组 ,如果栈为空指向 原来的nestToken
collector = tokenStack.length > 0 ? tokenStack[tokenStack.length -1][2] : nestTokens ;
break;
default:
collector.push(token)
}
}
return nestTokens;
}
处理“a.b.c”(lookup函数)
功能是可以在dataObj 对象中 ,寻找用连续点符号寻找keyName属性
比如 Obj = { a:{ b:{ c:100, } } }
那么lookup(dataObj, 'a.b.c') 结果是 100
/*
* 功能是可以在dataObj 对象中 ,寻找用连续点符号寻找keyName属性
比如 Obj = {
a:{
b:{
c:100,
}
}
}
那么lookup(dataObj, 'a.b.c') 结果是 100
* */
export default function lookup(dataObj,keyName){
// 判断keyName 是否含有 ’.‘ 但是又不能是 ’ . ‘
if (keyName.indexOf('.') !== -1 && keyName.trim() !== '.'){
//如果有“ . ” 就拆开
let keyNameArr = keyName.split('.')
//设置临时变量一层一层往下找
let temp = dataObj;
for (let index in keyNameArr ){
temp = temp[keyNameArr[index]];
}
return temp ;
}
return dataObj[keyName];
}
递归处理嵌套数据(parseArray方法)
/*
* 处理数组,结合renderTemplate 实现递归
* @token 是tokens的一个子项
* token 是什么, 就是一个简单的["#", 'student',[] ]
*
* 这个函数要递归调用 renderTemplate函数 ,调用多少次
* 比如data是
* [
{"name":"xiaoming",age:23,sex:"男",hobbies:["游泳","344"],"show":false,a:{b:{c:12}}},
{"name":"2323",age:3,sex:"男",hobbies:[344],"show":true,a:{b:{c:12}}},
{"name":"q",age:23,sex:"男",hobbies:["游泳","344","游泳","344","游泳","344"],"show":false,a:{b:{c:12}}},
{"name":"weewe",age:23,sex:"男",hobbies:["游泳",true],"show":true,a:{b:{c:12}}},
]
那么parseArray()函数需要递归调用renderTemplate 四次
* */
import lookup from "./lookup";
import renderTemplate from "./renderTemplate";
export default function parseArray(token, data) {
let v = lookup(data,token[1]);
let resultStr = '';
for (let i = 0 ; i < v.length; i++){
// 补一个 “ . ” 识别
resultStr += renderTemplate(token[2],{
...v[i],
// 处理“.” 运算符
'.':v[i],
})
}
return resultStr
}
将 tokens 变成dom字符串(renderTemplate方法)
import lookup from "./lookup";
import parseArray from "./parseArray";
export default function renderTemplate(tokens,data){
//结果字符串
let resultStr = '';
// 遍历tokens
for (let tokensKey in tokens) {
let token = tokens[tokensKey];
if (token[0] === 'text') {
// console.log(token)
//拼起来
resultStr += token[1];
} else if (token[0] === 'name') {
//如果是 “name" 类型 防止a.b.c ;类型
resultStr += lookup(data, token[1]);
} else if (token[0] === '#') {
// renderTemplate(token,token[1])
resultStr += parseArray(token,data)
}
}
return resultStr
}