受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术。本系列文章将会带您由浅入深的全面认识模板引擎的概念、设计、分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎。关于模板引擎的概念,我去年在百度百科上录入了自己的解释(请参考:模板引擎)。老陈曾经自己开发了一套网鸟Asp.Net模板引擎,虽然我自己并不乐意去推广它,但这已经无法阻挡群友的喜爱了!
与置换型模板引擎不同的是,解释型模板引擎包含了一个专用解释器,有了解释器的存在就可以支持一些更加复杂而严谨的语法。熟悉设计模式的朋友此时此刻应该能够回想起来“解释器模式”,它是将一些业务封装成一个或多个命令,然后通过一个复杂的解释器(Interpreter)来解析执行来满足生产需求的。实际上,解释型模版引擎就是一个较为复杂的解释器模式的实现而已。具体到模板引擎技术上来,解释器的工作任务实际上就小了很多。按照数据封装模式的不同,解释型模板引擎分为命令解释器和反射解释器两个大类。
命令解释器
还记得置换型模板引擎中,我们在最后曾经处理了一个较为负载的Label“{CreationTime:yyyy年MM月dd日 HH:mm:ss}”吗?实际上,这个就是命令解释器的一种实现。此Label中的format部分可以看作是CreationTime的一个命令。有了命令,我们可以让Label去做更多的事情,这是显而易见的。
命令也可以理解为指令。
为了更加能够说明这个问题,我们先将我们要处理的模板复杂化一点,如下:
1 <!--#include file="_Page_Header.shtml" --> 2 <ul> 3 <li>博主姓名:陈彦铭</li> 4 <li>创建日期:{CreationTime:yyyy年MM月dd日 HH:mm:ss}</li> 5 <li>粉丝数量:{FunsCount:D4}</li> 6 </ul> 7 <!--#include file="_Page_Footer.shtml" -->
除了我们已经熟悉的{xxx}标签之外,我们又增加了一个文件包含指令“<!--#include file="_Page_Header.shtml" -->”。这个指令是标准的SHTML语法,意思就是将其他文档中的模板合并到当前的指令位置,如此我们便可以实现代码重用。尤其对于HTML开发来说,使用SHTML包含语法,还可以在诸如Dreamweaver等IDE中实现“所见即可得”的效果。
关于如何实现包含指令,我们将在后续章节中具体介绍。
反射解释器
接触了指令之后,其实我们还是不够满足,因为到目前为止,我们的模板只能做简单的标签置换,这个相当不给力。比如有些内容我们想让它根据情况输出,那么就需要条件语句的效果,再如我们经常会遇到列表等情况,此时要是能有个迭代语句岂不是很给力?难道我们要编写一堆的Label1、Label2、Label3?
有需求,是好事情!咱们是做技术的,为需求而生,为满足需求而战,没有需求,技术就都只是浮云!
为什么条件语句和迭代语句要使用反射来实现,因为在编写这篇文章之前,老陈已经决定了用反射来实现这些机制,无论您是否喜欢!Hah..
我们再次将模板语法复杂化如下:
1 <!--#include file="_Page_Header.shtml" --> 2 <ul> 3 <li>博主姓名:陈彦铭</li> 4 <li>创建日期:{CreationTime:yyyy年MM月dd日 HH:mm:ss}</li> 5 <li>粉丝数量:{FunsCount:D4}</li> 6 </ul> 7 <hr /> 8 <m:if test="{articles.Count > 0}"> 9 <ul> 10 <m:foreach item="article" collection="articles"> 11 <li><a href="{article.Url}">[{article.Category}] {article.Title}</a></li> 12 </m:foreach> 13 </ul> 14 </m:if> 15 <!--#include file="_Page_Footer.shtml" -->
现在有木有感觉到某种给力呢?虽然这不是最给力的……
概念
虽然,我们只增加了两种语法,而且不多,看起来也貌似并不复杂,但这背后将会牵扯到方方面面的问题,代码解析的难度进一步增加。我们有必要了解一下后面可能使用到的一些概念。
- 元素:即Element。它是组成模板的最基本的元素,派生出标签、表达式、Tag等。
- 标签:即Label。形如{item},或{item.getTitle()}等,标签不能嵌套使用,但标签内的语法可以较为复杂;
- 表达式:即Expression。比如标签大括号之间的内容,我们从本节课开始将其称之为表达式,表达式是可以运行、计算的,例如我们可以输入较为复杂的标签:{item.x * item.y};
- 运算符:有了表达式,那么我们就需要一些运算符支持,例如上述{item.x * item.y}中的 * 运算符;
- Tag:我们将Tag定义为形如<m:if></m:if>或<m:else/>这样的表示形式,它的主要作用是提供流程控制支持;
- 特性:既然有Tag了,那么我们就需要特性,即Attribute,概念与xml/html中的Attribute一致,它通常为Tag提供可选参数;
- 指令:即Command,其格式类似于Tag,只不过开始和结束标记被我们替换为xml/html的注释格式,形如:<!--#include file="_Page_Footer.shtml" -->;
这里我列出的概念可能还不够完整,随着课程的演进,我们再一一认识他们!
再次提醒:如果之前的课程您没有掌握,请一定多多研究、体会,如果有问题可以随时通过评论提问,我会尽力解答!希望大家都能够真正的看懂、学会!
优点
解释型模板引擎可以支持更加复杂的语法、指令,能够满足更多的需求。同时由于它不像编译型模板引擎那样会受到运行时环境的限制,解释型模版引擎用途非常广泛。
缺点
但是它有一个很大的缺陷就是其内部使用了反射机制,虽然我们也试图对反射进行优化(请参考:浅谈.NET反射机制的性能优化),但这并不能完全解决反射带来的一系列问题,这就意味着在某些场合它无法获得令人满意。
小结
本课仅作为解释型模板引擎的一个引子,也是我写博过程中的一个缓冲。
在解析本课提出的两个模板代码的时候,您需要认真的体会我们之前的课程并学会它们,否则越往后面您就会越发的感觉到吃力!
为了自己,不要偷懒!学习是自己的,不学就一定不是自己的!
解释型模板引擎在我归纳的三大类模板引擎当中是难度最高的,我会细心的完成每一节课程,进度会比较慢,大家一定要耐着性子!我们的目标是看懂、学会、掌握,而不是直接应用它们!话说,楼主这一番苦心,大家是否表示赞同呢?
受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术。本系列文章将会带您由浅入深的全面认识模板引擎的概念、设计、分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎。关于模板引擎的概念,我去年在百度百科上录入了自己的解释(请参考:模板引擎)。老陈曾经自己开发了一套网鸟Asp.Net模板引擎,虽然我自己并不乐意去推广它,但这已经无法阻挡群友的喜爱了!
概述
本课我们主要讨论“命令解释器”的实现。命令就是指令,指令也是构成更加复杂的模板引擎的基本元素之一。至此我们可以归纳出来,模板引擎在工作的过程中,首先将字符流转换为Token流,然后再将Token流转换为Element集合(也算是流),然后将特定的Element单独拿出来或组合在一起形成指令、语句等。写一个模板引擎,和写一个小型的编译器几乎相当,因此我们需要耐心、细心!
目标
解析并运行如下模板代码结构:
- /_Page_Footer.shtml
- /_Page_Header.shtml
- /_Public_Footer.shtml
- /_Public_Header.shtml
- /Index.shtml
文件"/_Page_Footer.shtml"包含的代码:
<!--#include file="_Public_Footer.shtml" -->
文件"/_Page_Header.shtml"包含的代码:
<!--#include file="_Public_Header.shtml" -->
文件"/_Public_Footer.shtml"包含的代码:
</body> </html>
文件"/_Public_Header.shtml"包含的代码:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>{UserName}的博客</title> </head> <body>
文件"/Index.shtml"包含的代码:
1 <!--#include file="_Page_Header.shtml" --> 2 <ul> 3 <li>博主姓名:{UserName}</li> 4 <li>创建日期:{CreationTime:yyyy年MM月dd日 HH:mm:ss}</li> 5 <li>粉丝数量:{FunsCount:D4}</li> 6 </ul> 7 <!--#include file="_Page_Footer.shtml" -->
今天的模板内容被切分成了5个部分,嵌套层次达到了3层,解析难度比较大。实际上在编写本文之前,我自己的解释型模板引擎在内部是使用正则表达式的方式来实现嵌套指令解析的。不过,我们今天不会这么做了!
使用正则表达式实现
本节课的目的是说明命令解释器的实现,本着循序渐进的原则,我们首先考虑使用正则表达式来实现“<!--#include file="_Page_Header.shtml" -->”命令,在以后的课程中我们将会学习更加复杂的代码解析方法。
我们需要按照顺序使用正则表达式递归的读取和合并代码文档,具体实现如下:
1 /// <summary> 2 /// 表示 #Include 命令解释器。 3 /// </summary> 4 public static class IncludeCommandParser 5 { 6 private static int _nestedCount; 7 8 /// <summary> 9 /// 处置包含文档。 10 /// </summary> 11 /// <param name="templateString">包含模板代码的字符串。</param> 12 /// <param name="basePath">处置包含命令时要使用的基准路径。</param> 13 /// <returns>返回 <see cref="string"/>。</returns> 14 public static string Parse(string templateString, string basePath) 15 { 16 if (String.IsNullOrWhiteSpace(templateString)) return String.Empty; 17 if (String.IsNullOrWhiteSpace(basePath)) return templateString; 18 19 if (Directory.Exists(basePath) == false) throw new DirectoryNotFoundException(); 20 21 return _ProcessSSIElement(templateString, basePath); 22 } 23 24 private static string _ProcessSSIElement(string templateString, string basePath) 25 { 26 if (_nestedCount > 10) return templateString; 27 28 var matches = Regex.Matches(templateString, @"<!--#include file=""([^""]+)""\s*-->", RegexOptions.IgnoreCase | RegexOptions.Singleline); 29 30 foreach (Match match in matches) 31 { 32 var file = new FileInfo(Path.Combine(basePath, match.Groups[1].Value.Replace('/', '\\'))); 33 34 if (file.Exists == false) continue; 35 36 var subTemplate = File.ReadAllText(file.FullName).Trim(); 37 38 subTemplate = _ProcessSSIElement(subTemplate, Path.GetDirectoryName(file.FullName)); 39 40 templateString = templateString.Replace(match.Groups[0].Value, subTemplate); 41 } 42 43 _nestedCount++; 44 45 return templateString; 46 } 47 }
测试代码:
1 [Test] 2 public void LoadFileTest() 3 { 4 var fileName = Path.Combine(Environment.CurrentDirectory, "Templates\\Index.shtml"); 5 6 Assert.AreEqual(File.Exists(fileName), true); 7 8 this._templateString = File.ReadAllText(fileName); 9 10 Assert.NotNull(this._templateString); 11 12 Trace.WriteLine(this._templateString); 13 14 Assert.Greater(this._templateString.IndexOf("{CreationTime:yyyy年MM月dd日 HH:mm:ss}", StringComparison.Ordinal), 0); 15 } 16 17 [Test] 18 public void ProcessTest() 19 { 20 this.LoadFileTest(); 21 22 Trace.WriteLine("本次输出:"); 23 24 var basePath = Path.Combine(Environment.CurrentDirectory, "Templates"); 25 var templateEngine = TemplateEngine.FromString(this._templateString, basePath); 26 27 templateEngine.SetVariable("url", "http://www.ymind.net/"); 28 templateEngine.SetVariable("UserName", "陈彦铭"); 29 templateEngine.SetVariable("title", "陈彦铭的博客"); 30 templateEngine.SetVariable("FunsCount", 98); 31 templateEngine.SetVariable("CreationTime", new DateTime(2012, 4, 3, 16, 30, 24)); 32 33 var html = templateEngine.Process(); 34 Trace.WriteLine(html); 35 }
运行结果:
1 <!--#include file="_Page_Header.shtml" --> 2 <ul> 3 <li>博主姓名:{UserName}</li> 4 <li>创建日期:{CreationTime:yyyy年MM月dd日 HH:mm:ss}</li> 5 <li>粉丝数量:{FunsCount:D4}个</li> 6 </ul> 7 <!--#include file="_Page_Footer.shtml" --> 8 9 本次输出: 10 <!DOCTYPE HTML> 11 <html> 12 <head> 13 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 14 <title>陈彦铭的博客</title> 15 </head> 16 <body> 17 <ul> 18 <li>博主姓名:陈彦铭</li> 19 <li>创建日期:2012年04月03日 16:30:24</li> 20 <li>粉丝数量:0098个</li> 21 </ul> 22 </body> 23 </html>
运行结果达到了我们的期望值!
总结和代码下载
本课只是简单的介绍命令解释器的实现思路,实际上还有其他很多办法可以实现。
从下节课开始,我们将会接触到更多代码标记的解析方式,每篇博文篇幅不会太长,但一定会挑重点、击中要害!
本节课的内容较为简单,不提供代码下载。
模板引擎系列教程的规模比原来预想的还要庞大,因为我不想仅仅帖出各种代码就了事,希望能从更多的角度给大家分享。因此,该系列文章以后全部划入周末写作。平时只写文字性内容,或小篇幅技术文章。
希望大家能够谅解!
解释型模板引擎与置换型模板引擎最大的区别是它支持流程控制语句,其次是支持对象访问(反射实现)。今天我们的任务是设计模板语法,给我们的后续开发提供依据。
模板语法类型
- 模板标记;
- 模板指令;
- 注释性指令;
- 语句型指令;
- 条件语句;
- 遍历语句;
模板语法设计
- 模板标记:
- {title};
- {item.Name};
- {item.GetSize()};
- {item.GetObject("string", false, 0, 0.12, DateTime.Now)};
- 模板指令:
- 注释性指令:
- <!--#include file="../public_header.shtml" -->,用于引入包含文档;
- 语句型指令:
- <m:using css="../styles/common.css" combin="true" compress="false" />,为HTML量身定制,用于引入CSS文档,这个可以满足CSS文件拆分、合并、压缩等需要;
- <m:using js="../styles/common.css" combin="true" compress="false" />,为HTML量身定制,用于引入JS文档;
- <m:var name="age" value="30" />,理解为"30";
- <m:var name="age" value="{30}" />,理解为数字30;
- <m:var name="age" value="{DateTime.Now}" />;
- 注释性指令:
- 条件语句:
- <m:if test="">...</m:if>;
- <m:else />;
- <m:elseif test="" />;
- 遍历语句:
- <m:for from="0" to="10" index="i">...</m:for>
- <m:foreach var="item" collection="items" index="i">...</m:foreach>
总结
本节课没有任何实战性内容,只是总结了一下几天来我们完成的一些语法设计。这样的语法其实很好理解,SGML格式的。只不过解析起来就没有那么爽了!在构建解析过程的时候我自己都觉得非常痛苦,一不小心就会搞错,甚至不知道错在哪里了!
后续课程有两种方式展现给大家,一种是我直接帖出代码实现,一种是按照如上整理的内容拆解成章节一一详述。由于我拿不定注意,第一种方案怕大家学不到东西,第二种方案太慢。所以做个调查:持续关注本系列博文的同学请在评论里回复支持哪一种写作方案。