查找最好的模板引擎,发现这个搜索词出来的是beetl,于是就仔细学习了Beetl,试图找寻“最好的”三个字表现在哪里?于是搭建环境,阅读代码,与鄙人所做的TinyTemplate进行了粗略的对比,在征得beetl作者@闲.大赋 的同意后,编写了此对比文章。由于时间关系,对Beetl的认知深度还有不足,分析不当之处在所难免,还请广大同学纠正,定当有错误和不当必改。
1 | git clone https://git.oschina.net/xiandafu/beetl2.0.git |
不一会儿,输出了下面的内容
1 2 3 4 5 6 7 |
Cloning into 'beetl2.0'... remote: Counting objects: 5807, done. remote: Compressing objects: 100% (2145/2145), done. remote: Total 5807 (delta 3050), reused 5383 (delta 2733) Receiving objects: 100% (5807/5807), 14.60 MiB | 684.00 KiB/s, done. Resolving deltas: 100% (3050/3050), done. Checking connectivity... done. |
嗯嗯,好的开头是成功的一半,不错,代码取下来了。
1 2 |
cd beetl2.0 mvn install |
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[WARNING] [WARNING] Some problems were encountered while building the effective settings [WARNING] 'servers.server.id' must be unique but found duplicate server with id tiny-nexus-releases @ /Users/luoguo/Develop/apache-maven-3.1.0/conf/settings.xml [WARNING] [INFO] Scanning for projects... [ERROR] The build could not read 1 project -> [Help 1] [ERROR] [ERROR] The project org.beetl:beetl-core:2.2.4-SNAPSHOT (/Users/luoguo/git/beetl2.0/beetl-core/pom.xml) has 1 error [ERROR] Non-resolvable parent POM: Could not find artifact org.beetl:beetl-parent:pom:2.2.4-SNAPSHOT and 'parent.relativePath' points at wrong local POM @ line 4, column 10 -> [Help 2] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluen ... ctBuildingException [ERROR] [Help 2] http://cwiki.apache.org/confluen ... vableModelException |
咦,这是什么鬼? 猜想是由于我用的是maven 3.1.x导致,于是升级到maven 3.3.3,执行 mvn install,可以看到开始下载相关的资源文件了,OK,起步还是不错的,这里需要耐心等待一段时间。
咦,停止了,看到一堆错误,再看看是什么问题?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[ERROR] /Users/luoguo/git/beetl2.0/beetl-core/src/main/java/org/beetl/ext/jodd/BeetlActionResult.java:[13,8] org.beetl.ext.jodd.BeetlActionResult不是抽象的, 并且未覆盖jodd.madvoc.result.ActionResult中的抽象方法getResultType() [ERROR] /Users/luoguo/git/beetl2.0/beetl-core/src/main/java/org/beetl/ext/jodd/BeetlActionResult.java:[60,9] 方法不会覆盖或实现超类型的方法 [INFO] 2 errors [INFO] ------------------------------------------------------------- [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary: [INFO] [INFO] beetl-core ......................................... FAILURE [ 44.926 s] [INFO] beetl-parent ....................................... SKIPPED [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 45.061 s [INFO] Finished at: 2015-07-28T14:08:38+08:00 [INFO] Final Memory: 18M/262M [INFO] ------------------------------------------------------------------------ [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project beetl-core: Compilation failure: Compilation failure: [ERROR] /Users/luoguo/git/beetl2.0/beetl-core/src/main/java/org/beetl/ext/jodd/BeetlActionResult.java:[13,8] org.beetl.ext.jodd.BeetlActionResult不是抽象的, 并且未覆盖jodd.madvoc.result.ActionResult中的抽象方法getResultType() [ERROR] /Users/luoguo/git/beetl2.0/beetl-core/src/main/java/org/beetl/ext/jodd/BeetlActionResult.java:[60,9] 方法不会覆盖或实现超类型的方法 [ERROR] -> [Help 1] [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluen ... ojoFailureException |
看起来是Beetl继承了jodd的类,但是有些方法没有实现。 没有办法,只要增加fae指令再来执行:
1 | mvn clean install -fae |
1 2 3 4 5 6 7 8 9 10 11 |
[INFO] Reactor Summary: [INFO] [INFO] beetl-core ......................................... SUCCESS [03:52 min] [INFO] beetl-parent ....................................... SUCCESS [ 0.008 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 03:52 min [INFO] Finished at: 2015-07-28T14:26:09+08:00 [INFO] Final Memory: 25M/309M [INFO] ------------------------------------------------------------------------ |
从这里看,整体来说还可以,把一些bak文件上传上来,稍嫌不严谨,另外有些jpg文件直接放在根目录也有一点点乱,如果整理一下就更好了。
接下来比较关心core
这里面有几个东东,就有点难理解了,为什么这里放了个jar文件?为什么这里放了个lib目录?为什么这里放了个performance工程?性能评测的代码怎么会放到core工程中??
上面这个应该就是关键工程了?core应该就是引擎核心代码所在的位置,ext应该是它对各种开源框架方面的扩展或支持。有这些扩展还是非常不错的,方便使用者上手,赞一个。但是把ext和core放在一个工程里还是有点随意了,如果能把ext单独开个工程就更好了。
从上面的目录结构看还是不错的,但是很显然下面的一些类和接口看起来就比较乱了,应该相当有改进的空间。 相对应的,可以看看Tiny模板引擎的目录结构:
就简洁清爽多了。
再来看看beetl模板的代码行数:
可以看到core工程中的java代码是20291行,不算空行,不算注释行。
Tiny模板引擎的代码行数,纯纯的java代码只有4944行,也就是beetl的代码整整是Tiny模板引擎4倍多。
上面是Beetl的sonar检查情况
上面的统计数据是Tiny模板引擎的统计数据:
这里的数据和上面用Statistics统计的数据稍有区别,但是基本上差别不大。
从上面的数据可以看出:
项目 | Beetl | Tiny模板引擎 |
代码行数 | 23087 | 4944 |
文件数 | 230 | 171 |
重复 | 3.3% | 0.0% |
复杂度 | 2.8/方法 | 1.9/方法 |
包耦合指数 | 31.5% | 31.6% |
包耦合循环 | >35 | >18 |
Tiny模板引擎则完全遵守规范。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
statement : block #blockSt | textStatment #textOutputSt | constantsTextStatment #staticOutputSt | COMMENT_TAG commentTypeTag #commentTagSt | If parExpression statement (Else statement)? #ifSt | For LEFT_PAR forControl RIGHT_PAR statement ( Elsefor statement)? #forSt | While parExpression statement #whileSt | Switch parExpression switchBlock #siwchSt | Select g_switchStatment #selectSt | Try block (Catch LEFT_PAR Identifier? RIGHT_PAR block )? #trySt | Return expression? END #returnSt | Break END #breakSt | Continue END #continueSt | Var varDeclareList END #varSt | Directive directiveExp #directiveSt | assignMent END #assignSt | functionTagCall #functionTagSt | statementExpression END #statmentExpSt | Ajax Identifier COLON block #ajaxSt | END #end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
directive : set_directive | if_directive | while_directive | for_directive | break_directive | import_directive | continue_directive | stop_directive | include_directive | macro_directive | layout_directive | layout_impl_directive | call_block_directive | call_directive | endofline_directive | blank_directive | tabs_directive | indent_directive | dent_directive | call_macro_directive | call_macro_block_directive | bodycontent_directive | invalid_directive ; |
项目 | Beetl | Tiny |
定义临时变量 | var number=1 | #set(number=1) |
定义页面变量 | template.binding("number",1) | #!set(number=1) |
属性引用 | ${user.wife.name} | ${user.wife.name} |
算述表达式 | <% var a1 = 12; var b1 = (a1+15)/3-2*a1; var bc = -1-b1; %> ${bc} |
#set(a1=12,b1 = (a1+15)/3-2*a1,bc = -1-b1) ${bc} 当然,#set指令也可以一行写一个赋值指令 |
逻辑表达式 | <% var a1 = 12; var b1 = a1==12; var b2 = a1!=12; %> ${b1} ${b2} |
#set(a1 = 12,b1 = a1==12,b2 = a1!=12) ${b1} ${b2} |
循环语句 | <% print("总共"+userList.~size+" "); for(user in userList){ %> ${userLP.index} ${user.name} <%}%> |
总共${userList.size()} #for(user in userList) ${userFor.index} ${user.name} #end |
条件语句 | <% var user = map["001"]; if(user.name=="lijz"){ print(user.name); }else{ return ; } %> |
#set(user = map."001") #if(user.name=="lijz") ${user.name} #else #return #end |
函数调用 | <% print("hello"); println("hello"); printf("hello,%s,your age is %s","lijz",12+""); %> |
${format("hello")} ${format("hello\n")}
${format("hello,%s,your age is %s","lijz",12)}
|
格式化 | <% var now = date(); var date = date("2013-1-1","yyyy-MM-dd"); %> now=${now,dateFormat='yyyy年MM月dd日'} date=${date,dateFormat='yyyy年MM月dd日'} or now=${now,'yyyy年MM月dd日'} |
tiny模板引擎不允许动态创建对象,但是允许通过自定义函数或SpringBean来获取对象。 假设,这里在上下文中在now和date两个变量 now=${format(now,'yyyy年MM月dd日 HH:mm:SS')} date=${format(date,'yyyy年MM月dd日')} |
成员方法调用 | <% var list = [5,2,4]; %> ${ @java.util.Collections.max(list)} |
#set( list = [5,2,4]) ${list.get(1)} |
安全输出 | <% var user1 = null; var user2 = null; var user3 = {"name":"lijz",wife:{'name':'lucy'}}; %> ${user1.wife.name!"单身"} ${user2.wife.name!} ${user3.wife.name!"单身"} |
#set(user1 = null,user2 = null,user3 = {"name":"lijz",wife:{'name':'lucy'}})
${user1?.wife?.name?:"单身"}%> ${user2?.wife?.name?:"单身"} ${user3?.wife?.name?:"单身"} |
注释 | <% //最大值是12; /*最大值是12*/ var max = 12; %> |
##最大值是12;
#*最大值是12*# #set( max = 12) |
项目 | Beetl | Tiny模板引擎 | ||||||||||||||||
异常处理 | <% try{ callOtherSystemView() }catch(error){ print("暂时无数据"); } %> |
Tiny模板引擎的设计者认为如果让模板引擎来处理异常,实际上是有点过度设计的意味,而应该是系统的异常处理框架去处理之。模板只参与展示层的处理,不参与业务逻辑处理。 | ||||||||||||||||
虚拟属性 |
|
${user.toJson()} Tiny支持为某种类增加一些扩展的成员函数,和Beetl的虚拟属性的意思是相同的,但是在函数调用过程中,使用方式与原生成员函数没有区别。如果扩展的方法是getXxx,那么就可以直接调用object.xxx的方式按属性的方式来进行调用。 |
||||||||||||||||
函数扩展 | <% var date = date(); var len = strutil.len("cbd"); println("len="+len); %> |
Tiny也提供了函数扩展体系,也完全可以添加类似的函数扩展,调用方式也差不多。 #set(date =date(),len=strutil.len("cbd")) |
||||||||||||||||
标签的支持 | public class CmsContentTag extends GeneralVarTagBinding { public void render(){ Object id= this.getAttributeValue("id"); try {ctx.byteWriter.writeString("当前定义了一个窜上:"+id.toString()); }catch (IOException e){ e.printStackTrace(); } } } |
Tiny没有提供标签的扩展功能,却提供了强大的宏定义功能 简单宏定义
调用方式:
带内容宏定义 前置信息
调用方式:
运行结果:
由于Tiny采用的是全部在模板语言中实现的方式,因此定义和使用文本内容更方便,同时在定义和使用时的嵌套支持能力会使得DRY原则得以全面实施,可以整个页面没有重复内容的出现。 |
||||||||||||||||
布局支持 | content.html内容如下:
layout.html 是布局文件
运行结果: 运行content.html模板文件后,,正文文件的内容将被替换到layoutContent的地方,变成如下内容
|
Tiny的做法是: 首先新建content.layout文件
再新建content.page文件
然后访问content.page,运行结果就是:
实际上Tiny模板引擎还支持默认布局,多重布局各种花样玩样,由于采用了COC的方式,所以不需要在模板语言中显式引入布局,而是通过目录结构的方式来确定布局渲染方式。在进行重构的时候更也加方便,比如:同样一个文件,放在不同的目录结构中,由于渲染的布局不同,就会出现完全不一样的效果,这在进行重构的时候也更加方便。 Tiny在.layout中还支持指令#layout,如下:
上面就定义了两个布局占位,一个叫aaaInfo,一个叫bbbInfo, 在具体的页面文件中,可以用:
来覆盖默认的定义,转而显示新的内容,如果不覆盖的话,就显示默认的信息,这里通过引入Java的OverRide的机制,提供了更灵活多变的布局能力。 |
||||||||||||||||
宏引入 | 由于Tiny支持把公用的宏用独立的文件来进行存放,相当于Library,但是由于不同的人定义的库有可能有宏名冲突。因此Tiny引入了#import指令来优先使用先import进来的库中的宏,如下:
如果出现同名的宏,那么liba中的会被执行 |
|||||||||||||||||
安全调用 | Beetl采用的是安全表达式的方式来处理安全谳用 | Tiny的在调用属性或成员函数时,可以显式用“?.”来表示安全属性调用,而用“.”来表示非安全属性调用,这样写模板时需要明确使用哪个,这样可以及时发现应用中的问题。 | ||||||||||||||||
错误提示 |
错误提示如下:
beetl只给出了具体的位置在哪一行,以及整个模板(或者比较近位置的模板)内容。 |
错误提示如下:
Tiny则明确给出了精确的坐标,x1,y1-x2,y2,同时还给出了具体出问题的内容,相对来说程序员查找问题更加迅捷。 |
||||||||||||||||
由于篇幅太长,因此这里不贴完整内容,详细请看链接:http://my.oschina.net/tinyframework/blog/365370
OK,工具上完全不在一个等级上。
代码质量对比
代码质量这个本身没有唯一标准,这里贴一下类似的功能的代码对比,不做评论:
for语句实现 Beetl版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
public final class ForStatement extends Statement implements IGoto { public Expression idNode; public Expression exp; public Statement forPart; public Statement elseforPart; public boolean hasGoto = false; public short itType = 0; public boolean hasSafe; /** * for(idNode in exp) {forPath}elsefor{elseforPart} * @param idNode * @param exp * @param forPart * @param elseforPart * @param token */ public ForStatement(VarDefineNode idNode, Expression exp, boolean hasSafe, Statement forPart, Statement elseforPart, GrammarToken token) { super(token); this.idNode = idNode; this.exp = exp; this.hasSafe = hasSafe; this.elseforPart = elseforPart; this.forPart = forPart; } public final void execute(Context ctx) { // idNode 是其后设置的 int varIndex = ((IVarIndex) idNode).getVarIndex(); Object collection = exp.evaluate(ctx); IteratorStatus it = null; if (collection == null) { if (!this.hasSafe) { BeetlException ex = new BeetlException(BeetlException.NULL); ex.pushToken(exp.token); throw ex; } else { it = new IteratorStatus(Collections.EMPTY_LIST); } } else { it = IteratorStatus.getIteratorStatusByType(collection, itType); if (it == null) { BeetlParserException ex = new BeetlParserException(BeetlParserException.COLLECTION_EXPECTED_ERROR); ex.pushToken(exp.token); throw ex; } } ctx.vars[varIndex + 1] = it; // loop_index // ctx.vars[varIndex+2] = 0; // ctx.vars[varIndex+3] = it.getSize(); // if (this.hasGoto) { while (it.hasNext()) { ctx.vars[varIndex] = it.next(); forPart.execute(ctx); switch (ctx.gotoFlag) { case IGoto.NORMAL: break; case IGoto.CONTINUE: ctx.gotoFlag = IGoto.NORMAL; continue; case IGoto.RETURN: return; case IGoto.BREAK: ctx.gotoFlag = IGoto.NORMAL; return; } } if (!it.hasData()) { if (elseforPart != null) elseforPart.execute(ctx); } return; } else { while (it.hasNext()) { ctx.vars[varIndex] = it.next(); forPart.execute(ctx); } if (!it.hasData()) { if (elseforPart != null) elseforPart.execute(ctx); } } } @Override public final boolean hasGoto() { // TODO Auto-generated method stub return hasGoto; } @Override public final void setGoto(boolean occour) { this.hasGoto = occour; } @Override public void infer(InferContext inferCtx) { exp.infer(inferCtx); if (exp.getType().types != null) { if (Map.class.isAssignableFrom(exp.getType().cls)) { idNode.type = Type.mapEntryType; } else { //list or array idNode.type = exp.getType().types[0]; } } else { idNode.type = Type.ObjectType; } int index = ((IVarIndex) idNode).getVarIndex(); inferCtx.types[index] = idNode.type; inferCtx.types[index + 1] = new Type(IteratorStatus.class, idNode.type.cls); forPart.infer(inferCtx); if (elseforPart != null) { elseforPart.infer(inferCtx); } } } |
Tiny版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class ForProcessor implements ContextProcessor public Class return TinyTemplateParser.For_directiveContext.class; } public boolean processChildren() { return false; } public Object process(TemplateInterpreter interpreter, TemplateFromContext templateFromContext, TinyTemplateParser.For_directiveContext parseTree, TemplateContext pageContext, TemplateContext context, TemplateEngineDefault engine, Writer writer, String fileName) throws Exception { String name = parseTree.for_expression().IDENTIFIER().getText(); Object values = interpreter.interpretTree(engine, templateFromContext, parseTree.for_expression().expression(),pageContext, context, writer,fileName); ForIterator forIterator = new ForIterator(values); context.put(name + "For", forIterator); boolean hasItem = false; while (forIterator.hasNext()) { TemplateContext forContext=new TemplateContextDefault(); forContext.setParent(context); hasItem = true; Object value = forIterator.next(); forContext.put(name, value); try { interpreter.interpretTree(engine, templateFromContext, parseTree.block(),pageContext, forContext, writer,fileName ); } catch (ForBreakException be) { break; } catch (ForContinueException ce) { continue; } } if (!hasItem) { TinyTemplateParser.Else_directiveContext elseDirectiveContext = parseTree.else_directive(); if (elseDirectiveContext != null) { interpreter.interpretTree(engine, templateFromContext, elseDirectiveContext.block(), pageContext,context, writer,fileName); } } return null; } } |
解释引擎核心处理代码 Beetl版 beetl版源代码,由于太长,所以就不贴内容了,详细请点击查看源码
Tiny版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
public class TemplateInterpreter { TerminalNodeProcessor[] terminalNodeProcessors = new TerminalNodeProcessor[200]; Map OtherTerminalNodeProcessor otherNodeProcessor = new OtherTerminalNodeProcessor(); public void addTerminalNodeProcessor(TerminalNodeProcessor processor) { terminalNodeProcessors[processor.getType()] = processor; } public void addContextProcessor(ContextProcessor contextProcessor) { contextProcessorMap.put(contextProcessor.getType(), contextProcessor); } public TinyTemplateParser.TemplateContext parserTemplateTree(String sourceName, String templateString) { char[] source = templateString.toCharArray(); ANTLRInputStream is = new ANTLRInputStream(source, source.length); // set source file name, it will be displayed in error report. is.name = sourceName; TinyTemplateParser parser = new TinyTemplateParser(new CommonTokenStream(new TinyTemplateLexer(is))); return parser.template(); } public void interpret(TemplateEngineDefault engine, TemplateFromContext templateFromContext, String templateString, String sourceName, TemplateContext pageContext, TemplateContext context, Writer writer, String fileName) throws Exception { interpret(engine, templateFromContext, parserTemplateTree(sourceName, templateString), pageContext, context, writer,fileName ); writer.flush(); } public void interpret(TemplateEngineDefault engine, TemplateFromContext templateFromContext, TinyTemplateParser.TemplateContext templateParseTree, TemplateContext pageContext, TemplateContext context, Writer writer, String fileName) throws Exception { for (int i = 0; i < templateParseTree.getChildCount(); i++) { interpretTree(engine, templateFromContext, templateParseTree.getChild(i), pageContext, context, writer,fileName ); } } public Object interpretTree(TemplateEngineDefault engine, TemplateFromContext templateFromContext, ParseTree tree, TemplateContext pageContext, TemplateContext context, Writer writer, String fileName) throws Exception { Object returnValue = null; if (tree instanceof TerminalNode) { TerminalNode terminalNode = (TerminalNode) tree; TerminalNodeProcessor processor = terminalNodeProcessors[terminalNode.getSymbol().getType()]; if (processor != null) { returnValue = processor.process(terminalNode, context, writer); } else { returnValue = otherNodeProcessor.process(terminalNode, context, writer); } } else if (tree instanceof ParserRuleContext) { try { ContextProcessor processor = contextProcessorMap.get(tree.getClass()); if (processor != null) { returnValue = processor.process(this, templateFromContext, (ParserRuleContext) tree, pageContext, context, engine, writer,fileName); } if (processor == null || processor != null && processor.processChildren()) { for (int i = 0; i < tree.getChildCount(); i++) { Object value = interpretTree(engine, templateFromContext, tree.getChild(i), pageContext, context, writer,fileName ); if (value != null) { returnValue = value; } } } } catch (StopException se) { throw se; } catch (TemplateException te) { if (te.getContext() == null) { te.setContext((ParserRuleContext) tree,fileName); } throw te; } catch (Exception e) { throw new TemplateException(e, (ParserRuleContext) tree,fileName); } } else { for (int i = 0; i < tree.getChildCount(); i++) { Object value = interpretTree(engine, templateFromContext, tree.getChild(i), pageContext, context, writer,fileName ); if (returnValue == null && value != null) { returnValue = value; } } } return returnValue; } public static void write(Writer writer, Object object) throws IOException { if (object != null) { writer.write(object.toString()); } } } |
嗯嗯,不到100行的规模
当然整个通读下来,就会慢慢发现为什么Tiny的代码行数这么少功能却又多的原因之所在了。
总结
Beetl算得上是较好的模板语言框架和不错的开源项目,但是距离“最好的”三个字还是有一定差距的,作为@闲.大赋 的粉丝,偶会持续支持他,也希望他能再积再累,真正当得起“最好的”三个字。
补充说明
beetl里面有4014行由antlr生成的代码,实际统计中,由于Beetl的目录结构没有按标准化的来,导致统计中包含了这部分代码,因此实际上,应该是在16000+,因此规模是Tiny模板引擎的3倍左右,特此纠正。
欢迎访问开源技术社区:http://bbs.tinygroup.org。本例涉及的代码和框架资料,将会在社区分享。《自己动手写框架》成员QQ群:228977971,一起动手,了解开源框架的奥秘!或点击加入QQ群:http://jq.qq.com/?_wv=1027&k=d0myfX