You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functionparseTemplate(template,tags=sugar.tags){if(!template)return[]letsections=[]// Stack to hold section tokenslettokens=[]// Buffer to hold the tokensletspaces=[]// Indices of whitespace tokens on the current linelethasTag=false// Is there a {{tag}} on the current line?letnonSpace=false// Is there a non-space char on the current line?letopeningTagReletclosingTagReletclosingCurlyRe// 解析tags, 生成 openingTagRe, closingTagRe, closingCurlyRe 这3个正则,// 分别用来检测 开始标签(一般"{{"),结束标签(一般"}}"),raw输出结束标签(一般"}}}")compileTags(tags)// 用template创建Scanner实例constscanner=newScanner(template)letstart,type,value,chr,token,openSection// 条件:只要template没有处理完while(!scanner.eos()){start=scanner.pos// 当前处理位置,初始 0// (1)把开始标签前的字符串截取出来(开始标签前的必然是纯粹的text)value=scanner.scanUntil(openingTagRe)if(value){for(leti=0,valueLength=value.length;i<valueLength;++i){chr=value.charAt(i)// 如果字符是空白,把index(tokens数组位置)放到spaces数组中if(isWhitespace(chr)){spaces.push(tokens.length)}else{nonSpace=true// nonSpace标志设为true}// 作为text类型的token放到tokens数组tokens.push(['text',chr,start,++start])// 如果是换行,那么检查整行,看是否需要把空白删掉if(chr==='\n')stripSpace()}}// (2)截取开始标签,如果没有,跳出whileif(!scanner.scan(openingTagRe)){break}// 设置 hasTag 标志为 true,因为开始标签之后的就是标签类型及内容了hasTag=true// (3)截取标签类型,可能是`#,^,/,>,{,&,=,!`中的一种,如果都不是,那么就是 nametype=scanner.scan(tagRe)||'name'// (4) 截取(删掉)可能的空白scanner.scan(whiteRe)// (5) 根据标签类型来获取标签的内容// 类型是 = ,用于切换开始结束标签,形式类似 {{=<% %>=}}if(type==='='){// 所以我们把结束的 = 前的截取出来才是标签内容value=scanner.scanUntil(equalsRe)scanner.scan(equalsRe)scanner.scanUntil(closingTagRe)}// 类型是 { ,表示内容不用转译,原样输出,形式类似 {{{name}}}elseif(type==='{'){// 所以我们必须把 '}}}' 前的字符串截取出来作为标签内容value=scanner.scanUntil(closingCurlyRe)scanner.scan(curlyRe)scanner.scanUntil(closingTagRe)type='&'}// 其他类型下结束标签 '}}' 前的就是内容else{value=scanner.scanUntil(closingTagRe)}// (6) 截取/删掉结束标签if(!scanner.scan(closingTagRe))thrownewError('Unclosed tag at '+scanner.pos)// 构造token并pushtoken=[type,value,start,scanner.pos]tokens.push(token)// 根据类型做一些额外处理if(type==='#'||type==='^'){sections.push(token)// 如果是section类的开始(#,^),push 到sections}// 如果是section类的结束(/),pop sections 并校验section完整性elseif(type==='/'){// Check section nesting.openSection=sections.pop()if(!openSection)thrownewError('Unopened section "'+value+'" at '+start)if(openSection[1]!==value)thrownewError('Unclosed section "'+openSection[1]+'" at '+start)}// 对于 name,{,& ,说明需要输出字符,这一行就是 nonSpace 的elseif(type==='name'||type==='{'||type==='&'){nonSpace=true}// 对于 = ,重新解析开始结束标签,以供下面继续解析时更换开始结束标签正则elseif(type==='='){compileTags(value)}}// 保证template处理完后不会剩余section,否则就是模板中有未闭合的sectionopenSection=sections.pop()if(openSection)thrownewError('Unclosed section "'+openSection[1]+'" at '+scanner.pos)returnnestTokens(squashTokens(tokens))// 不必多说,就是设置3个正则 openingTagRe,closingTagRe,closingCurlyRefunctioncompileTags(tagsToCompile){if(typeoftagsToCompile==='string')tagsToCompile=tagsToCompile.split(spaceRe,2)if(!isArray(tagsToCompile)||tagsToCompile.length!==2)thrownewError('Invalid tags: '+tagsToCompile)openingTagRe=newRegExp(escapeRegExp(tagsToCompile[0])+'\\s*')closingTagRe=newRegExp('\\s*'+escapeRegExp(tagsToCompile[1]))closingCurlyRe=newRegExp('\\s*'+escapeRegExp('}'+tagsToCompile[1]))}// 如果某行只有section开始/结束标签,那么删除这行的所有空白// 比如 1. {{#tag}} 2. {{/tag}} 这种,因为它们所在行如果只有空白加标签,那么空白是// 无意义的,并且不应该影响最终生成的字符串functionstripSpace(){if(hasTag&&!nonSpace){while(spaces.length){tokens[spaces.pop()]=null}}else{spaces=[]}hasTag=falsenonSpace=false}}
前端模板
mustache.js
源码解析近些年各种前端模板引擎层出不穷,
mustache
就是其中比较出名的一种。mustache
是一种弱逻辑的模板语法,mustache.js
是它的JS实现。为什么关注
mustache.js
并去解析源码?underscore template
和Micro-Templating
等等模板是基于原生JS语法,解析基本是运用正则拼接字符串;相比它们,mustache.js
基于自定义语法,解析更为复杂。解读mustache.js
可以学习自定义语法的解析(简单的手写解析器)。mustache.js
本身代码精简(v2.2.1
只有600+行),结构清晰,易于理解。mustache.js
的进阶版有handlebars
等等,可以在mustache.js
基础上自己定制/增强前端模板。源码分析
600多行的
mustache.js
大致可以分为以下几部分:以上是我对代码按功能分块重构(
ES6
)后的文件组织形式,其中index.js
主要暴露API,utils.js
是一些工具函数,这里都省略掉,重点要讲的就是context.js/parser.js/scanner.js/writer.js
四部分。完整的代码可以在项目sugar-template
中查看。scanner.js
Scanner
类很简单,主要功能是扫描字符串,按指定正则分割字符串。类只有3个方法,都很简短,稍微讲解
scanUntil
和scan
。两者都接受一个正则作为参数,其中scanUntil
是把符合正则部分之前的字符串切分出来。假设this.tail.search(re) --> index
,函数返回this.tail.slice(0, index)
。scan
是把符合正则部分(必须从开头符合正则)的字符串切分出来。两个方法都用来按正则截取字符串,并内部处理
pos
标记位置。parser.js
结下来讲
parser.js
,它是Scanner
类的使用者,用于把模板解析为token
树。由于这段代码较长,所以解析放在代码注释里。
parseTemplate
应该算比较长了,但总体来说并不复杂,就是完成一个template string ---> tokens
的转换。token
的格式是:[type, value, startIndex, endIndex]
。另外:
squashTokens
函数的作用是合并text token
;nestTokens
函数的作用是把tokens
转化成tokens tree
。([type, value, startIndex, endIndex, innerTokens]
)。总体来说两个函数都不难,这里只讲解下
nestTokens
:context.js
到这里我们已经获得了
tokens
,那么怎么从tokens + data ---> html
?别急,先完成一个依赖任务,怎么处理这个
data
?有人会问,
data
要处理吗?不同模板引擎有不同的态度,mustache
处理data
后可以做到:怎么说呢,
Context
的作用清晰简单,并没有什么需要特别讲解。下面以一个例子明确下它的作用:writer.js
writer.js
主要是Writer
类,负责tokens + data ---> html
。Writer
类主要需要关注的是render**
形式的方法,来一个例子加深理解:
render**
基本也没复杂逻辑,只要注意2点:true
时不用构造)去递归render;rawValue|escapedValue|unescapedValue
3个基础方法。结语
对
mustache.js
的源码解析到这里结束,希望看文章的各位没被我误导;如有不对或有疑问,也请直接回复 😄The text was updated successfully, but these errors were encountered: