GRMustache源码解析
Intro
GRMustache是一个第三方开源框架,用于支持XML(HTML)文件动态生成,提供模版定义。
具体支持特性:http://mustache.github.io/mustache.5.html
Demo
GRMustache模板如下:
{{header}}
{{#bug}}
{{/bug}}
{{#items}}
{{#first}}
{{name}}
{{/first}}
{{#link}}
{{name}}
{{/link}}
{{/items}}
{{#empty}}
The list is empty.
{{/empty}}
对应的数据源为JSON对象:
{
"header": "Colors",
"items": [
{"name": "red", "first": true, "url": "#Red"},
{"name": "green", "link": true, "url": "#Green"},
{"name": "blue", "link": true, "url": "#Blue"}
],
"empty": false
}
通过GRMustache生成出来的HTML为:
Colors
red green blue
源码分析
GRMustache中,由模板和JSON对象到XML文档的过程可以粗略概括如下两个步骤:
首先从Client/Server获取模板文件TemplateDocName.mustache生成GRMustacheTemplate实例对象。
GRMustacheTemplate实例对象负责读取JSON对象,然后渲染成XML字符串返回给Caller。
Generating Template
GRMustacheTemplate只是一个容器类,里面包含了一个重要的类Repository,保存在一个栈中,栈顶是模板当前环境对应的Repository。
Repository是执行具体将抽象模板实例化的类,此外还负责加载模板。这里面封装了TemplateAST这个类,这个类就是保存模板信息的模型类,一个TemplateAST对应一个TemplateID,内部可以根据ID对AST进行索引查找和缓存。
- (GRMustacheTemplateAST *)templateASTFromString:(NSString *)templateString contentType:(GRMustacheContentType)contentType templateID:(id)templateID error:(NSError **)error
在Repository的上述方法中进行了TemplateAST的具体生成工作:
实例化GRMustacheCompiler和GRMustacheTemplateParser。
将complier设置为parser的delegate。
执行parser的parseTemplateString方法。
从compiler中获取TemplateAST。
parseTemplateString方法
parseTemplateString方法是这个部分的关键执行方法,使用了一个基于状态机的字符串模式匹配算法(?)来进行。这个算法设置了五个状态机:
State Machine Internal State | Description |
---|---|
stateStart | 未知状态,根据下一个字符来判断状态去向 |
stateText | 正在遍历纯文本 |
stateTag | 正在遍历{{tag}} |
stateUnescapedTag | 正在遍历{{{tag}}} |
stateSetDelimitersTag | 正在遍历{{=tag=}} |
接下来从模板文本的第一个字符开始进行字符串的遍历,起始状态为stateStart。
如图:
其中stateText状态转移到其他状态的条件与stateStart的转移条件一致,不再在图中描绘。重点关注红色箭头,当状态机运转到红色箭头的条件发生时,说明了一个tag已经被完全遍历完,如:{{name}},此时需要进行事务逻辑的处理。这个时候引入了一个新的对象叫GRMustacheToken来记录此时的状态,包括:range, innerRange和type等。其中type是一个用于记录当前tag类型的enum类型变量。然后parser通过delegate的回调方法将token传给代理(compiler)来对已标识的字符串进行处理。
Compiler的shouldContinueAfterParsingToken方法
上一部分中的parser已经完成了模板文本中对每一个结点(标签)的标识工作——给文本和标签作类型判断并且记录位置,这些信息被保存在Token中传入了Comipiler的shouldContinueAfterParsingToken方法中进行处理。接下来介绍一个重要的数据结构——GRMustacheTemplateASTNode。
GRMustacheTemplateASTNode是一个抽象类,也就是一个协议,其子类包含了TextNode和Tag,其中Tag又包含了VariableTag和SectionTag。GRMustacheVariableTag和GRMustacheTextNode都是作为最小单位结点不可再分地存在,而GRMustacheSectionTag对应的是{{#Tag}}这种类型的结点,里面可以嵌套其他结点和列表,如果递归的看待这个过程,那就可以把SectionTag里面的内容设计为另一个TemplateAST类的一个实例(子树)。综上所述,VariableTag和TextNode可以看为AST的叶子结点,而SectionTag则可以看为AST的一颗子树。
在Compiler中生成的一颗完整的TemplateAST树可以描述为下图:
在compiler内部执行的是一颗树的生成过程,内部保存了三个栈结构:tagValueStack,openingTokenStack,ASTNodesStack。解析过程如下:
parser将带有文本信息的token通过代理方法传递给compiler。
compiler做了以下事情,用伪代码描述一下:
switch token.type:
case sectionOpening:
this.currentOpeningToken = token;
tagValueStack.push(token.value);
openingTokenStack.push(token.value);
this.nodes = new Nodes();
ASTNodesStack.push(nodes);
case Text:
this.currentNodes.append(new TextNode(token));
case Variable:
this.currentNodes.append(new Variable(token));
case Closing:
TemplateAST tree = TemplateAST(this.currentNodes);
this.tagValueStack.pop();
this.openingTokenStack.pop();
this.ASTNodesStack.pop();
this.currentxxx = xxxxStack.top(); //取栈顶元素
this.currentNodes.append(new ASTNodes(tree));
最后compiler的除了ASTNodesStack(因为这个栈在初始化的时候就被先压入一个元素)之外的两个栈的元素将全部被弹出,然后使用最后nodeStack中的元素生成一棵树作为解析结果返回,整个过程完成。
Binding Template With Object
GRMustache通过实现模版和数据的绑定来实现XML文件的生成。调用者通过调用GRMustacheTemplate的:
- (NSString *)renderObject:(id)object error:(NSError **)error
方法来实现这个过程,返回的NSString就是最后生成的布局文件字符串。这个方法中调用了:
- (NSString *)renderContentWithContext:(GRMustacheContext *)context HTMLSafe:(BOOL *)HTMLSafe error:(NSError **)error
来执行下一步的操作。在这个方法中根据当前templateRepository初始化一个GRMustacheRenderingEngine,这个实例来负责具体的事务执行,类似上文中的parser和compiler的职责。在RenderingEngine中,首先作了以下几件事情(粗略):
申请了一个能够容纳1024个Unicode字符的结构体作为buffer存储解析结果。
判断当前入参的templateAST的contentType是否和engine的contentType一致,若不一致则根据入参的contentType初始化一个新的renderingEngine来处理解析事务;如果一致则进入3。
遍历当前templateAST的子结点数组,并对每一个子结点执行以下操作:
处理partialNode
对每个结点执行GRMustacheTemplateASTNode协议中的acceptTemplateASTVisitor方法
acceptTemplateASTVistior调用visitor也就是engine的visit方法解析对应结点的数据
首先看Section和Variable对相关代理方法的实现:
对于Section/Variable Tag而言,最后这个协议方法会回到visitor,也就是RenderingEngine的下面方法中执行具体的解析过程:
- (BOOL)visitTag:(GRMustacheTag *)tag expression:(GRMustacheExpression *)expression escapesHTML:(BOOL)escapesHTML error:(NSError **)error
其中GRMustacheExpression是GRMustacheTag内部的一个属性,里面保存了Token。接着看源码:
// Render value
//value 就是传入的需要绑定的数据 通常是一个json序列化出来的NSDictionary的实例
id renderingObject = [GRMustacheRendering renderingObjectForObject:value];
NSString *rendering = nil;
NSError *renderingError = nil; // Default nil, so that we can help lazy coders who return nil as a valid rendering.
BOOL HTMLSafe = NO; // Default NO, so that we assume unsafe rendering from lazy coders who do not explicitly set it.
switch (tag.type) {
// 通过type 走不同逻辑
case GRMustacheTagTypeVariable:
rendering = [renderingObject renderForMustacheTag:tag context:context HTMLSafe:&HTMLSafe error:&renderingError];
break;
case GRMustacheTagTypeSection: {
// section 先判断 对应的value是否有值
BOOL boolValue = [renderingObject mustacheBoolValue];
if (!tag.isInverted != !boolValue) {
// important calling
rendering = [renderingObject renderForMustacheTag:tag context:context HTMLSafe:&HTMLSafe error:&renderingError];
} else {
rendering = @"";
}
} break;
}
renderingObject对应着入参的Object,在Objective-C环境下,通常会是以下几种类型:NSString,NSObject,NSNumber。renderingObject调用了协议规定的成员方法renderForMustacheTag:,并且把tag当前对应的tag和context传入。在[GRMustacheRendering initialize]方法中使用runtime给以上的类型都绑定上这个协议方法。
renderForMustacheTag:
接下来已NSObject和NSString两个常见的类型为例来看下在runtime绑定的render方法中的实现:
- NSObject:
static NSString *GRMustacheRenderWithIterationSupportNSObject(NSObject *self, SEL _cmd, GRMustacheTag *tag, BOOL enumerationItem, GRMustacheContext *context, BOOL *HTMLSafe, NSError **error)
static NSString *GRMustacheRenderWithIterationSupportNSObject(NSObject *self, SEL _cmd, GRMustacheTag *tag, BOOL enumerationItem, GRMustacheContext *context, BOOL *HTMLSafe, NSError **error)
{
switch (tag.type) {
case GRMustacheTagTypeVariable:
// {{ object }}
if (HTMLSafe != NULL) {
*HTMLSafe = NO;
}
return [self description];
case GRMustacheTagTypeSection:
// {{# object }}...{{/}}
// {{^ object }}...{{/}}
context = [context newContextByAddingObject:self];
NSString *rendering = [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];
[context release];
return rendering;
}
}
- NSString:
static NSString *GRMustacheRenderWithIterationSupportNSString(NSString *self, SEL _cmd, GRMustacheTag *tag, BOOL enumerationItem, GRMustacheContext *context, BOOL *HTMLSafe, NSError **error)
{
switch (tag.type) {
case GRMustacheTagTypeVariable:
// {{ string }}
if (HTMLSafe != NULL) {
*HTMLSafe = NO;
}
return self;
case GRMustacheTagTypeSection:
if (tag.isInverted) {
// {{^ number }}...{{/}}
return [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];
} else {
// {{# string }}...{{/}}
context = [context newContextByAddingObject:self];
NSString *rendering = [tag renderContentWithContext:context HTMLSafe:HTMLSafe error:error];
[context release];
return rendering;
}
}
}
在Rendering方法中根据tag.type来作出不同的逻辑判断:如果是Variable类型,也就是已经是叶子结点了,直接返回自身的描述,如{{name}} 对应的object为 {"name" : "tom"} 渲染结果直接为 tom到对应的标签位;如果是一个子树的的父节点,也就是Section类型,调用GRMustacheTag的renderContentWithContext方法返回渲染的结果,方法的实现如下:
- (NSString *)renderContentWithContext:(GRMustacheContext *)context HTMLSafe:(BOOL *)HTMLSafe error:(NSError **)error
{
if (HTMLSafe) {
*HTMLSafe = (_contentType == GRMustacheContentTypeHTML);
}
return @"";
}
可以看到最后直接返回空字符串,这样符合预期:当section字段的值非空的时候,section字段的标签位置:{{#name}}不需要做渲染。如Demo中的items标签是不需要渲染的。
- 对于textNode的解析更为直接,textNode的acceptTemplateASTVisitor方法会直接调用engine的以下成员方法:
- (BOOL)visitTextNode:(GRMustacheTextNode *)textNode error:(NSError **)error
{
GRMustacheBufferAppendString(&_buffer, textNode.text);
return YES;
}
直接Append textNode.text的内容在渲染结果字符串后面。于是整个将模版和数据绑定的流程就打通了,可以总结为以下流程图:
图片挂掉了。。传不上去。