受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术。本系列文章将会带您由浅入深的全面认识模板引擎的概念、设计、分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎。关于模板引擎的概念,我去年在百度百科上录入了自己的解释(请参考:模板引擎)。老陈曾经自己开发了一套网鸟Asp.Net模板引擎,虽然我自己并不乐意去推广它,但这已经无法阻挡群友的喜爱了!

上次我们简单的认识了一下置换型模板引擎的几种情况,当然我总结的可能不够完善,希望大家继续补充。谈到按流替代式模板引擎的原理但并没有给出真正的实现。跟帖的评论中有一位朋友(Treenew Lyn)说的很好:“Token 解析其实是按一个字符一个字符去解析的”。的确是这样,而且唯有这样才能够实现更加高效、更加准确的模板引擎机制。我们首先将模板代码分解成一个一个的Token,然后按照顺序形成Token流(顺序集合),在输出的时候替换规定好的语法标记即可。

目的

假定我们要处理的模板文件类似于如下格式(与上一节一样):

1 /// <summary>
2 /// 模板文本。
3 /// </summary>
4 public const string TEMPLATE_STRING = @"<a href=""{url}"">{title}</a><br />";

我们的目的是将它按照{xxx}这样的标记分解成Token流。

方案

解决这个问题的方案大致有这么几种:

  1. 最直观的就是正则表达式;
  2. 比较拐弯但比正则快的就是split+各种技巧;
  3. 按(字符)流解析 ,即将字符流转化为Token流;

今天我们只讨论第三种情况,第一种很简单,第二种稍微复杂一点,但相信难不倒您!第三种做法只是不太常见,但如果您接触过编译原理(或搜索引擎开发),就不是那么陌生了。

思路

首先,我们看看这段模板代码按字符流输出回是怎样的:

 1 // 实现代码
 2 [Test]
 3 public void Test1()
 4 {
 5     var s = new StringBuilder();
 6
 7     foreach (var c in TestObjects.TEMPLATE_STRING)
 8     {
 9         // 这里我们用回车换行符将输出隔开
10         s.AppendLine(c.ToString(CultureInfo.InvariantCulture));
11     }
12
13     Trace.WriteLine(s.ToString());
14 }
15
16 /* 输出结果如下
17 <
18 a
19
20 h
21 r
22 e
23 f
24 =
25 "
26 {
27 u
28 r
29 l
30 }
31 "
32 >
33 {
34 t
35 i
36 t
37 l
38 e
39 }
40 <
41 /
42 a
43 >
44 <
45 b
46 r
47
48 /
49 >
50 */

这个结果显然与我们期望的相差很远(请留意飘红的字符们),其实我们需要的结果是这样的:

1 <a href="
2 {url}
3 ">
4 {title}
5 </a><br />

基本上我们可以总结出如下规律(为了容易理解,我们这里只考虑{xxx}标记):

  1. 从开始到"{"之前的部分算作一个Token;
  2. "{"、"}"之间的部分(含"{"、"}")算作一个Token;
  3. "}"、"{"之间的部分(不含"}"、"{")算作一个Token;
  4. "}"到结尾的部分算作一个Token;

思路有了,那么算法如何实现呢?为了避免篇幅过长,我这里直接给出一个有限状态机的解决方案。为了更加直观的理解这个问题,请您现在将鼠标定位在字符串"<a href=""{url}"">{title}</a><br />"的开始处,然后使用方向键向右移动光标,观察光标在每个位置(pos)的状态,图解如下:

这里出现了4个状态,分别是“开始”、“进入目标”、“脱离目标”和“结束”。而在实际编码过程中,我们通常忽略开始和结束,因为这两个状态始终都是需要处理的,而且各有且仅有1次,直接硬编码实现即可。

题外话:如果您实在难以理解什么是“有限状态机”的话,那么你可以简单的理解为“状态有限的机器(制)”,虽然这么说是非常不准确的,但这个可以帮助你去思考这个概念。另外可以参考“状态机”。

将字符流转化为Token流的过程

要利用有限状态机,我们首先要定义一下业务状态:

 1 /// <summary>
 2 /// 定义解析模式(即状态)。
 3 /// </summary>
 4 public enum ParserMode
 5 {
 6     /// <summary>
 7 /// 无状态。
 8 /// </summary>
 9     None = 0,
10
11     /// <summary>
12 /// 进入标签处理。
13 /// </summary>
14     EnterLabel = 1,
15
16     /// <summary>
17 /// 退出标签处理。
18 /// </summary>
19     LeaveLabel = 2
20 }

在这里我们定义了三个状态,实际上只需要两个。None这个状态在实践中没有实际意义,只是为了在编码过程中让语义更加接近现实(面向对象编程中会有很多这种情况)。遇到"{"或"}"的时候就进行状态变换,而每次状态变换都需要做一些处理动作,下面是算法的主体骨架:

 1 // 这俩还需要解释??
 2 private const char _LABEL_OPEN_CHAR = '{';
 3 private const char _LABEL_CLOSE_CHAR = '}';
 4
 5 [Test]
 6 public void Test2()
 7 {
 8     var templateLength = TestObjects.TEMPLATE_STRING.Length;
 9
10     // 为了模拟光标的定位移动,我们在这里采用for而不是foreach
11 // 在本例中用for还是foreach都无关紧要
12 // 以后我们还会讨论更加复杂的情况,到时候就需要用到while(bool)了!
13     for (var index = 0; index < templateLength; index++)
14     {
15         var c = TestObjects.TEMPLATE_STRING[index];
16
17         switch (c)
18         {
19             case _LABEL_OPEN_CHAR:
20                 // ...
21                 this._EnterMode(ParserMode.EnterLabel);
22                 break;
23
24             case _LABEL_CLOSE_CHAR:
25                 // ...
26                 this._LeaveMode();
27                 break;
28
29             default:
30                 // ...
31                 break;
32         }
33     }
34
35     // 到达结尾的时候也需要处理寄存器中的内容
36 // 这就是之前提到的硬编码解决开始和结束两个状态
37 // ...
38 }

在状态变换之前,我们需要一系列的寄存器(临时变量)来存储当前状态、历史状态(限于本例就是上次状态)、历史数据以及处理成功的Token等,定义如下:

 1 /// <summary>
 2 /// 表示 Token 顺序集合(Token流)。
 3 /// </summary>
 4 private readonly List<string> _tokens = new List<string>();
 5
 6 // 为有限状态机定义一个寄存器
 7 // 注意:有限状态机的理解在物理层的电路上和在编程概念上是相通的
 8 private readonly StringBuilder _temp = new StringBuilder();
 9
10 /// <summary>
11 /// 表示当前状态。
12 /// </summary>
13 private ParserMode _currentMode;
14
15 /// <summary>
16 /// 表示上一状态。
17 /// </summary>
18 /// <remarks>
19 /// 如果状态多余两个的话,我们总不能再定义一个"_last_last_Mode"吧!
20 /// 在状态有多个的时候,需要使用 <see cref="Stack{T}"/> 来保存历史
21 /// 状态,这个我们将在解释型模版引擎中用到。
22 /// </remarks>
23 private ParserMode _lastMode;

切换模式的时候需要对各个寄存器做相应的处理,我的注释很详细就不解释了:

 1 /// <summary>
 2 /// 进入模式。
 3 /// </summary>
 4 /// <param name="mode"><see cref="ParserMode"/> 枚举值之一。</param>
 5 private void _EnterMode(ParserMode mode)
 6 {
 7     // 当状态改变的时候应当保存之前已处理的寄存器中的内容
 8     if (this._temp.Length > 0)
 9     {
10         this._tokens.Add(this._temp.ToString());
11
12         this._temp.Clear();
13     }
14
15     this._lastMode = this._currentMode;
16     this._currentMode = mode;
17 }
18
19 /// <summary>
20 /// 离开模式。
21 /// </summary>
22 private void _LeaveMode()
23 {
24     // 当状态改变的时候应当保存之前已处理的寄存器中的内容
25 // 当状态超过2个的时候,实际上这里的代码应该是不一样的
26 // 虽然现在我们只需要考虑两种状态,但为了更加直观的演示,我特意在这里又写了一遍
27     if (this._temp.Length > 0)
28     {
29         this._tokens.Add(this._temp.ToString());
30         this._temp.Clear();
31     }
32
33     // 因为只有两个状态,因此
34     this._currentMode = this._lastMode;
35 }

然后再完善一下之前提到的主体骨架,测试,输出结果如下:

1 <a href="
2 {url}
3 ">
4 {title}
5 </a><br />

我们得到了预期的结果!

将Token流输出为业务数据

在上一节中我们曾经提到过Token流输出时将标签置换为业务数据的思路,如果您忘记了,那么请回去再看看吧!

有了思路,那么实现就非常容易了,联合业务数据进行测试:

 1 [Test]
 2 public void Test3()
 3 {
 4     this.ParseTemplate(TestObjects.TEMPLATE_STRING);
 5
 6     foreach (var newsItem in TestObjects.NewsItems)
 7     {
 8         foreach (var token in this._tokens)
 9         {
10             switch (token)
11             {
12                 case "{url}":
13                     Trace.Write(newsItem.Key);
14                     break;
15
16                 case "{title}":
17                     Trace.Write(newsItem.Value);
18                     break;
19
20                 default:
21                     Trace.Write(token);
22                     break;
23             }
24         }
25
26         Trace.WriteLine(String.Empty);
27     }
28 }

经过测试输出结果完全正确!

搞定!

总结及代码下载

本文主要内容是阐述如何使用有限状态机这种机制来完成“从字符流向Token流”的转换的。不过本文为了降低入门门槛,一切举例和算法都从简,大家应该很容易上手!

要补充的是,本文并没有真正的去封装一个模板引擎,而仅仅是说明了其工作原理,我想这个比直接给大家一个已经实现的模板引擎要好的多,毕竟这是“渔”而不是“鱼”。

本文代码下载:置换型模板引擎(1-2).zip


下集预报:置换型模板引擎(三)将于清明节之后放出,届时将会封装一个简单但完整的基于“按流替代式”的模板引擎,达到实用级别。

另外,请大家不要催促我博文的写作,老陈毕竟不是打印机啊!哈哈!

由浅入深:自己动手开发模板引擎——置换型模板引擎(二)相关推荐

  1. 由浅入深:自己动手开发模板引擎——置换型模板引擎(四)

    受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术.本系列文章将会带您由浅入深的全面认识模板引擎的概念.设计.分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎.关于 ...

  2. 由浅入深:自己动手开发模板引擎——置换型模板引擎(三)

    受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术.本系列文章将会带您由浅入深的全面认识模板引擎的概念.设计.分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎.关于 ...

  3. 由浅入深:自己动手开发模板引擎——置换型模板引擎(一)

    受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术.本系列文章将会带您由浅入深的全面认识模板引擎的概念.设计.分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎.关于 ...

  4. 由浅入深:自己动手开发模板引擎——解释型模板引擎

    受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术.本系列文章将会带您由浅入深的全面认识模板引擎的概念.设计.分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎.关于 ...

  5. 由浅入深:自己动手开发模板引擎——解释型模板引擎(二)

    受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术.本系列文章将会带您由浅入深的全面认识模板引擎的概念.设计.分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎.关于 ...

  6. freemarker ftl模板_Web开发人员必会的模板引擎技术之Freemarker

    曾几何时,Web开发是个多么高大上的名字,程序猿们都以能搞定Web技术为荣,此时还没有前后端之说.然而随着互联网的发展,社会分工进一步细化,职业岗位也更加细分,慢慢开始有了前端攻城狮和后端攻城狮,技术 ...

  7. beetl模板使用场景_Java 模板引擎 Beetl 2.0 发布

    Java开源模板引擎 Velocity Velocity是一个基于java的模板引擎(template engine).它允许任何人仅仅简单的使用模板语言(template language)来引用由 ...

  8. Django 3.2.5博客开发教程:一些常用的模板使用方法

    一.django static文件的引入方式 1.在django project中创建 static文件夹 2.settings.py中配置要在 STATIC_URL = '/static/' 下边 ...

  9. Angular 原理图 Schematics 学习 - 动手开发一个实际的例子

    当 ng add 命令向项目中添加某个库时,就会运行原理图.ng generate 命令则会运行原理图,来创建应用.库和 Angular 代码块. 一些术语: 规则 在原理图 中,是指一个在文件树上运 ...

最新文章

  1. VTK:RenderMan之PolyDataRIB
  2. 年年岁岁花相似,岁岁年年竟相同
  3. 【转载】(EM算法)The EM Algorithm
  4. RookeyFrame 隐藏 首次加载菜单 的伸缩动画
  5. HDU 1251 - 统计难题(字典树模板题)
  6. 新华网评:企业的信息安全关把好了吗?
  7. 服务器虚拟机如何复制文件,Windows中复制虚拟机
  8. RuntimeError: Cannot re-initialize CUDA in forked subprocess. 一个奇怪bug的奇妙解决方法
  9. JAVA的面向对象编程
  10. jQuery便利多个相同的class,点击显示隐藏图标,显示及隐藏其下的内容
  11. 压缩图片大小至指定Kb以下
  12. AVUE 点击编辑按钮拉起弹窗前增加自己的操作
  13. 记半次元App数据解密记录
  14. 活动预告丨易盾CTO朱浩齐将出席2018 AIIA大会,分享《人工智能在内容安全的应用实践》...
  15. shell编程之iptables
  16. python绘制敏感性和特异性曲线(交叉)
  17. Spark1.5.2伪分布安装
  18. 惠而浦扫地机器人充不进电_惠而浦和西屋诉说不要被廉价欺骗 | 智能扫地机器人评测...
  19. Witt向量简介 §3.1:Witt多项式
  20. 我的同事们(一):Alex Peng

热门文章

  1. UVA10020(最小区间覆盖)
  2. hdu5056(找相同字母不出现k次的子串个数)
  3. LeetCode03:无重复字符的最长子串
  4. Google protobuf使用技巧和经验
  5. java判断时间为上午,中午,下午,晚上,凌晨
  6. 项目实战---模拟亿邦动力网
  7. Hadoop集群搭建及MapReduce应用
  8. python-管理MySQL之ConfigParser模块
  9. 多态基类与虚析构函数
  10. HDU 4445 Crazy Tank --枚举