内容简介:一般来说,通过使用
mustache.js
是一个弱逻辑的模板引擎,语法十分简单,使用很方便。源码( v2.2.1
)只有 600+
行,且代码结构清晰。
一般来说, mustache.js
使用方法如下:
var template = 'Hello, {{name}}'; var rendered = Mustache.render(template, { name: 'World' }); document.getElementById('container').innerHTML = rendered;
通过使用 Chrome
对上述 Mustache.render
的 debug
,我们顺藤摸瓜梳理了 mustache.js
5个模块(暂且称它们为: Utils
, Scanner
, Parser
, Writer
, Context
)间的关系图如下:
代码层面, Mustache.render()
方法是 mustache.js
向外暴露的方法之一,
mustache.render = function render(template, view, partials) { // 容错处理 if (typeof template !== 'string') { throw new TypeError('Invalid template! Template should be a "string" ' + 'but "' + typeStr(template) + '" was given as the first ' + 'argument for mustache#render(template, view, partials)'); } // 调用Writer.render return defaultWriter.render(template, view, partials); };
在其内部,它首先调用了 Writer.render()
方法,
Writer.prototype.render = function render(template, view, partials) { // 调用Writer构造器的parse方法 var tokens = this.parse(template); // 渲染逻辑,后文会分析 var context = (view instanceof Context) ? view : new Context(view); return this.renderTokens(tokens, context, partials, template); };
而 Writer.render()
方法首先调用了 Writer.parse()
方法,
Writer.prototype.parse = function parse(template, tags) { var cache = this.cache; var tokens = cache[template]; if (tokens == null) // 调用parseTemplate方法 tokens = cache[template] = parseTemplate(template, tags); return tokens; };
Writer.parse()
方法调用了 parseTemplate
方法,
所以,归根结底, Mustache.render()
方法首先调用 parseTemplate
方法对 html
字符串进行解析,
然后,将一个对象渲染到解析出来的模板中去。
所以,我们得研究源码核心所在—— parseTemplate
方法。在此之前,我们的先看一些前置方法:工具方法和扫描器。
工具方法( Utils
)
// 判断某个值是否为数组 var objectToString = Object.prototype.toString; var isArray = Array.isArray || function isArrayPolyfill(object) { return objectToString.call(object) === '[object Array]'; }; // 判断某个值是否为函数 function isFunction(object) { return typeof object === 'function'; } // 更精确的返回数组类型的typeof值为'array',而非默认的'object' function typeStr(obj) { return isArray(obj) ? 'array' : typeof obj; } // 转义正则表达式里的特殊字符 function escapeRegExp(string) { return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); } // 判断对象是否有某属性 function hasProperty(obj, propName) { return obj != null && typeof obj === 'object' && (propName in obj); } // 正则验证,防止 Linux 和Windows下不同spidermonkey版本导致的bug var regExpTest = RegExp.prototype.test; function testRegExp(re, string) { return regExpTest.call(re, string); } // 是否是空格 var nonSpaceRe = /\S/; function isWhitespace(string) { return !testRegExp(nonSpaceRe, string); } // 将特殊字符转为转义字符 var entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ' ', '/': '/', '`': '`', '=': '=' }; function escapeHtml(string) { return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) { return entityMap[s]; }); } var whiteRe = /\s*/; // 匹配0个以上的空格 var spaceRe = /\s+/; // 匹配1个以上的空格 var equalsRe = /\s*=/; // 匹配0个以上的空格加等号 var curlyRe = /\s*\}/; // 匹配0个以上的空格加} var tagRe = /#|\^|\/|>|\{|&|=|!/; // 匹配#,^,/,>,{,&,=,!
扫描器( Scanner
)
// Scanner构造器,用于扫描模板 function Scanner(string) { this.string = string; // 模板总字符串 this.tail = string; // 模板剩余待扫描字符串 this.pos = 0; // 扫描索引,即表示当前扫描到第几个字符串 } // 如果模板扫描完成,返回true Scanner.prototype.eos = function eos() { return this.tail === ''; }; // 扫描的下一批的字符串是否匹配re正则,如果不匹配或者match的index不为0 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; }; // 扫描到符合re正则匹配的字符串为止,将匹配之前的字符串返回,扫描索引设为扫描到的位置 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; };
总的来说,扫描器,就是用来扫描字符串的。扫描器中只有三个方法:
eos scan scanUntil
现在进入 parseTemplate
方法。
解析器( Parser
)
解析器是整个源码中最重要的方法,用于解析模板,将 html
标签与模板标签分离。
整个解析原理为:遍历字符串,通过正则以及扫描器,将普通 html
和模板标签扫描并且分离,并保存为数组 tokens
。
function parseTemplate(template, tags) { if (!template) return []; var sections = []; // 用于临时保存解析后的模板标签对象 var tokens = []; // 保存所有解析后的对象 var spaces = []; // 包括空格对象在tokens里的索引 var hasTag = false; // 当前行是否有{{tag}} var nonSpace = false; // 当前行是否有非空格字符 // 去除保存在tokens里的空格对象 function stripSpace() { if (hasTag && !nonSpace) { while (spaces.length) delete tokens[spaces.pop()]; } else { spaces = []; } hasTag = false; nonSpace = false; } var openingTagRe, closingTagRe, closingCurlyRe; // 将tag转换为正则,默认tag为{{和}},所以转成匹配{{的正则,和匹配}}的正则,以及匹配}}}的正则 // 因为mustache的解析中如果是{{{}}}里的内容则被解析为html代码 function compileTags(tagsToCompile) { if (typeof tagsToCompile === 'string') tagsToCompile = tagsToCompile.split(spaceRe, 2); if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) throw new Error('Invalid tags: ' + tagsToCompile); openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); } compileTags(tags || mustache.tags); var scanner = new Scanner(template); var start, type, value, chr, token, openSection; while (!scanner.eos()) { start = scanner.pos; // 开始扫描模板,扫描至{{时停止扫描,并且将此前扫描过的字符保存为value value = scanner.scanUntil(openingTagRe); if (value) { // 遍历{{之前的字符 for (var i = 0, valueLength = value.length; i < valueLength; ++i) { chr = value.charAt(i); // 如果当前字符为空格,这用spaces数组记录保存至tokens里的索引 if (isWhitespace(chr)) { spaces.push(tokens.length); } else { nonSpace = true; } tokens.push(['text', chr, start, start + 1]); start += 1; // 如果遇到换行符,则将前一行的空格清除 if (chr === '\n') stripSpace(); } } // 判断下一个字符串中是否有{{,同时更新扫描索引到{{的后一位 if (!scanner.scan(openingTagRe)) break; hasTag = true; // 扫描标签类型,是{{#}}还是{{=}}或其他 type = scanner.scan(tagRe) || 'name'; scanner.scan(whiteRe); // 根据标签类型获取标签里的值,同时通过扫描器,刷新扫描索引 if (type === '=') { value = scanner.scanUntil(equalsRe); // 使扫描索引更新为\s*=后 scanner.scan(equalsRe); // 使扫描索引更新为}}后,下面同理 scanner.scanUntil(closingTagRe); } else if (type === '{') { value = scanner.scanUntil(closingCurlyRe); scanner.scan(curlyRe); scanner.scanUntil(closingTagRe); type = '&'; } else { value = scanner.scanUntil(closingTagRe); } // 匹配模板闭合标签即}},如果没有匹配到则抛出异常, // 同时更新扫描索引至}}后一位,至此时即完成了一个模板标签{{#tag}}的扫描 if (!scanner.scan(closingTagRe)) throw new Error('Unclosed tag at ' + scanner.pos); // 将模板标签也保存至tokens数组中 token = [type, value, start, scanner.pos]; tokens.push(token); // 如果type为#或者^,也将tokens保存至sections if (type === '#' || type === '^') { sections.push(token); } else if (type === '/') { // 如果type为/则说明当前扫描到的模板标签为{{/tag}}, // 则判断是否有{{#tag}}与其对应 openSection = sections.pop(); // 检查模板标签是否闭合,{{#}}是否与{{/}}对应,即临时保存在sections最后的{{#tag}} if (!openSection) throw new Error('Unopened section "' + value + '" at ' + start); // 是否跟当前扫描到的{{/tag}}的tagName相同 if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); // 具体原理:扫描第一个tag,sections为[{{#tag}}], // 扫描第二个后sections为[{{#tag}}, {{#tag2}}], // 以此类推扫描多个开始tag后,sections为[{{#tag}}, {{#tag2}} ... {{#tag}}] // 所以接下来如果扫描到{{/tag}}则需跟sections的最后一个相对应才能算标签闭合。 // 同时比较后还需将sections的最后一个删除,才能进行下一轮比较。 } else if (type === 'name' || type === '{' || type === '&') { // 如果标签类型为name、{或&,不用清空上一行的空格 nonSpace = true; } else if (type === '=') { // 编译标签,为下一次循环做准备 compileTags(value); } } // 确保sections中没有开始标签 openSection = sections.pop(); if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); return nestTokens(squashTokens(tokens)); }
我们来看经过解析器解析之后得到的 tokens
的数据结构:
每一个子项都类似下面这种结构
token[0]
为 token
的类型,可能的值有 #
、 ^
、 /
、 &
、 name
、 text
,分别表示{}时,调用 renderSection
方法
Writer.prototype.renderSection = function renderSection(token, context, partials, originalTemplate) { var self = this; var buffer = ''; // 获取{{#xx}}中xx在传进来的对象里的值 var value = context.lookup(token[1]); function subRender(template) { return self.render(template, context, partials); } if (!value) return; if (isArray(value)) { // 如果为数组,说明要复写html,通过递归,获取数组里的渲染结果 for (var j = 0, valueLength = value.length; j < valueLength; ++j) { buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); } } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { // 如果value为对象或字符串或数字,则不用循环,根据value进入下一次递归 buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); } else if (isFunction(value)) { if (typeof originalTemplate !== 'string') throw new Error('Cannot use higher-order sections without the original template'); // 如果value是方法,则执行该方法,并且将返回值保存 value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); if (value != null) buffer += value; } else { // 如果不是上面所有情况,直接进入下次递归 buffer += this.renderTokens(token[4], context, partials, originalTemplate); } return buffer; };
当模板标签类型为时,说明要当 value
不存在( null
、 undefined
、 0
、 ''
)或者为空数组的时候才触发渲染。
看看 renderInverted
方法的实现
Writer.prototype.renderInverted = function renderInverted(token, context, partials, originalTemplate) { var value = context.lookup(token[1]); // 值为null,undefined,0,''或空数组 // 直接进入下次递归 if (!value || (isArray(value) && value.length === 0)) { return this.renderTokens(token[4], context, partials, originalTemplate); } };
结语
到这为止, mustache.js
的源码解析完了,可以看出来, mustache.js
最主要的是一个解析器和一个渲染器,以非常简洁的方式实现了一个强大的模板引擎。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
工程问题C++语言求解
Delores M.Etter、Jeanine A.Ingber / 冯力、周凯 / 机械工业出版社 / 2014-8 / 79元
本书介绍了如何利用ANSIC++编程语言以基于对象的编程方式来解决工程问题。书中引用了大量来自于不同工程、科学和计算机科学领域的示例,是一本理论和实践结合紧密的教材。针对C++基本语法的各个部分,由浅入深地进行讲解。每讲解一部分基础知识,同时会结合多个相关实例,实例内容详实,紧贴所讲内容,使读者能够立刻对所学知识进行练习,实战性强。一起来看看 《工程问题C++语言求解》 这本书的介绍吧!