GRMustache源码解析

GRMustache源码解析

Intro

GRMustache是一个第三方开源框架,用于支持XML(HTML)文件动态生成,提供模版定义。

具体支持特性:http://mustache.github.io/mustache.5.html

Demo

GRMustache模板如下:

<h1>{{header}}</h1>
{{#bug}}
{{/bug}}

{{#items}}
  {{#first}}
    <li><strong>{{name}}</strong></li>
  {{/first}}
  {{#link}}
    <li><a href="{{url}}">{{name}}</a></li>
  {{/link}}
{{/items}}

{{#empty}}
  <p>The list is empty.</p>
{{/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为:

<h1>Colors</h1><li><strong>red</strong></li><li><a href="#Green">green</a></li><li><a href="#Blue">blue</a></li>

源码分析

GRMustache中,由模板和JSON对象到XML文档的过程可以粗略概括如下两个步骤:

  1. 首先从Client/Server获取模板文件TemplateDocName.mustache生成GRMustacheTemplate实例对象。

  2. GRMustacheTemplate实例对象负责读取JSON对象,然后渲染成XML字符串返回给Caller。

Generating Template


Template.png
  1. GRMustacheTemplate只是一个容器类,里面包含了一个重要的类Repository,保存在一个栈中,栈顶是模板当前环境对应的Repository。

  2. Repository是执行具体将抽象模板实例化的类,此外还负责加载模板。这里面封装了TemplateAST这个类,这个类就是保存模板信息的模型类,一个TemplateAST对应一个TemplateID,内部可以根据ID对AST进行索引查找和缓存。


- (GRMustacheTemplateAST *)templateASTFromString:(NSString *)templateString contentType:(GRMustacheContentType)contentType templateID:(id)templateID error:(NSError **)error

在Repository的上述方法中进行了TemplateAST的具体生成工作:

  1. 实例化GRMustacheCompiler和GRMustacheTemplateParser。

  2. 将complier设置为parser的delegate。

  3. 执行parser的parseTemplateString方法。

  4. 从compiler中获取TemplateAST。

parseTemplateString方法

parseTemplateString方法是这个部分的关键执行方法,使用了一个基于状态机的字符串模式匹配算法(?)来进行。这个算法设置了五个状态机:

State Machine Internal State Description
stateStart 未知状态,根据下一个字符来判断状态去向
stateText 正在遍历纯文本
stateTag 正在遍历{{tag}}
stateUnescapedTag 正在遍历{{{tag}}}
stateSetDelimitersTag 正在遍历{{=tag=}}

接下来从模板文本的第一个字符开始进行字符串的遍历,起始状态为stateStart。

如图:


state machine.png

其中stateText状态转移到其他状态的条件与stateStart的转移条件一致,不再在图中描绘。重点关注红色箭头,当状态机运转到红色箭头的条件发生时,说明了一个tag已经被完全遍历完,如:{{name}},此时需要进行事务逻辑的处理。这个时候引入了一个新的对象叫GRMustacheToken来记录此时的状态,包括:range, innerRange和type等。其中type是一个用于记录当前tag类型的enum类型变量。然后parser通过delegate的回调方法将token传给代理(compiler)来对已标识的字符串进行处理。

Compiler的shouldContinueAfterParsingToken方法

上一部分中的parser已经完成了模板文本中对每一个结点(标签)的标识工作——给文本和标签作类型判断并且记录位置,这些信息被保存在Token中传入了Comipiler的shouldContinueAfterParsingToken方法中进行处理。接下来介绍一个重要的数据结构——GRMustacheTemplateASTNode。

ASTNodes.png

GRMustacheTemplateASTNode是一个抽象类,也就是一个协议,其子类包含了TextNode和Tag,其中Tag又包含了VariableTag和SectionTag。GRMustacheVariableTag和GRMustacheTextNode都是作为最小单位结点不可再分地存在,而GRMustacheSectionTag对应的是{{#Tag}}这种类型的结点,里面可以嵌套其他结点和列表,如果递归的看待这个过程,那就可以把SectionTag里面的内容设计为另一个TemplateAST类的一个实例(子树)。综上所述,VariableTag和TextNode可以看为AST的叶子结点,而SectionTag则可以看为AST的一颗子树。

在Compiler中生成的一颗完整的TemplateAST树可以描述为下图:

完整AST树.png

在compiler内部执行的是一颗树的生成过程,内部保存了三个栈结构:tagValueStack,openingTokenStack,ASTNodesStack。解析过程如下:

  1. parser将带有文本信息的token通过代理方法传递给compiler。

  2. 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中,首先作了以下几件事情(粗略):

  1. 申请了一个能够容纳1024个Unicode字符的结构体作为buffer存储解析结果。

  2. 判断当前入参的templateAST的contentType是否和engine的contentType一致,若不一致则根据入参的contentType初始化一个新的renderingEngine来处理解析事务;如果一致则进入3。

  3. 遍历当前templateAST的子结点数组,并对每一个子结点执行以下操作:

  4. 处理partialNode

  5. 对每个结点执行GRMustacheTemplateASTNode协议中的acceptTemplateASTVisitor方法

  6. 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<GRMustacheRendering> 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的内容在渲染结果字符串后面。于是整个将模版和数据绑定的流程就打通了,可以总结为以下流程图:
简书图片挂掉了。。传不上去。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容