由淺入深:自己動手開發模板引擎——解釋型模板引擎
受到群裡兄弟們的竭力邀請,老陳終於決定來分享一下.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>
執行結果達到了我們的期望值!
總結和程式碼下載
本課只是簡單的介紹命令直譯器的實現思路,實際上還有其他很多辦法可以實現。
從下節課開始,我們將會接觸到更多程式碼標記的解析方式,每篇博文篇幅不會太長,但一定會挑重點、擊中要害!
本節課的內容較為簡單,不提供程式碼下載。
模板引擎系列教程的規模比原來預想的還要龐大,因為我不想僅僅帖出各種程式碼就了事,希望能從更多的角度給大家分享。因此,該系列文章以後全部劃入週末寫作。平時只寫文字性內容,或小篇幅技術文章。
希望大家能夠諒解!