简单的 Web 页面模板解析器和模板池






4.40/5 (4投票s)
2004 年 8 月 17 日
6分钟阅读

64584

1556
一个简单的网页模板解析器,用于将布局与代码分离。
引言
ASP.NET 为我们提供了许多构建 Web 系统的简单方法,尤其是代码隐藏技术,它令人惊叹地实现了布局和代码的分离。然而,ASP.NET 也提供了一些机制,允许您构建比代码隐藏提供的功能更丰富的自定义编程模型。其中一种机制是 HTTP Handler,它提供了一种与 IIS Web 服务器的底层请求和响应服务交互的方式,并提供了类似于 ISAPI 扩展的功能,但编程模型更简单。太棒了!这种机制正是我最喜欢的,因为它给我一种一切尽在掌控之中,并且我自由自在的感觉。
但是当你编写自定义 HTTP 处理程序时,硬编码页面布局是枯燥且容易出错的。我们确实需要一种将布局与代码分离的方法。因此,TmplParser
类、TemplatePool
类等应运而生。TmplParser
类主要用于解析带有我自己定义的简单标签和标记的布局模板文件 :-)。TemplatePool
类用于缓冲一组模板,这可以减少 I/O 操作(即减少模板文件读取),从而提高性能。稍后我将在本文中演示如何使用它们来分离布局与代码。
注意:这是我第一次使用 ASP.NET 编程,第一次使用 C# 编程,第一次接触 IIS,也是第一次向 Code Project 提交文章。因此,文章和代码中肯定存在一些问题或不足之处。我非常欢迎任何反馈。谢谢!
使用代码
首先,我将向您介绍解析器的使用规则和一些基本信息。
- 模板文件只包含两个基本元素:
block
和label
。一个块由开始标记<!--BEGIN: BLOCKNAME -->
和结束标记<!--END: BLOCKNAME -->
定义。一个标签定义为{LABELNAME}
。让我们看一个示例模板文件 example.html,以便您更清楚地理解这些概念。example.html
<html> <head> <title> Example </title> </head> <body> <table> <!--BEGIN: FORMAT1 --> <tr><th>{THEAD1}</th></tr> <!--BEGIN: ROW1 --> <tr><td>{VALUE1}</td></tr> <!--END: ROW1 --> <!--END: FORMAT1 --> <!--BEGIN: FORMAT2 --> <tr><th>{THEAD1}</th><th>{THEAD2}</th></tr> <!--BEGIN: ROW2 --> <tr><td>{VALUE1}</td><td>{VALUE2}</td></tr> <!--END: ROW2 --> <!--END: FORMAT2 --> </table> </body> </html>
在模板文件 example.html 中,有四个块:
FORMAT1
、ROW1
、FORMAT2
和ROW2
。块FORMAT1
有一个标签:THEAD1
;块ROW1
有一个标签:VALUE1
;块FORMAT2
有两个标签:THEAD1
和THEAD2
;块ROW2
有两个标签:VALUE1
和VALUE2
。FORMAT1
和FORMAT2
分别是ROW1
和ROW2
的父块。实际上,还有一个块。它是root block
,即模板文件本身。我们称之为DOCUMENT-BLOCK
。 - 模板文件定义规则
- 块名在一个模板文件中必须是唯一的。这不仅是为了代码可读性,也是为了便于使用。我的原则是:越简单越好。
- 一个块可以通过包含其他块而成为父块。但任何两个块都不能重叠。并且没有兄弟关系,只维护块之间的父子关系。
- 深入探究
当模板文件被
TmplParser
解析时,如下面的代码所示// tmplDir is the path of the directory in which example.html is placed. // It's in the format like this: E:/Demo/tmpl/ // 50 is the capacity of the pool. // The pool uses LRU algorithm for template replacement. TemplatePool.Singleton(tmplDir, 50); // What GetTemplate does is to load the template file, // create and initialize a new instance // of TmplParser for this template file, // do the parsing and return it if the filename // passed is not found in the pool, // otherwise just return a clone // of the instance found in pool. // It's thread-safe. // ITemplate is an interface inherited by TmplParser. ITemplate tmpl = TemplatePool.Singleton().GetTemplate("example.html"); // the second time, just return a cloned instance // for template example.html // The code below is just for instruction. ITemplate tmpl2 = TemplatePool.Singleton().GetTemplate("example.html");
然后将构建五个
BlockParser
(一个嵌套在TmplParser
中的私有类)实例,并由TmplParser
维护。这五个块的内容如下ROW1
<tr><td>{VALUE1}</td></tr>
ROW2
<tr><td>{VALUE1}</td><td>{VALUE2}</td></tr>
FORMAT1
<tr><th>{THEAD1}</th></tr> <tag:ROW1/>
FORMAT2
<tr><th>{THEAD1}</th><th>{THEAD2}</th></tr> <tag:ROW2/>
DOCUMENT-BLOCK
<html> <head> <title> Example </title> </head> <body> <table> <tag:FORMAT1/> <tag:FORMAT2/> </table> </body> </html>
一旦子块(例如:
ROW1
)被输出,其当前内容将被放置在其父块中标签<tag:BLOCKNAME/>
(例如:<tag:ROW1/>
)之前,并且它将返回到原始的原始内容。执行以下代码后,
DOCUMENT-BLOCK
的内容将是 ...ITmplBlock tmplROW1 = tmpl.ParseBlock("ROW1"); ITmplBlock tmplFORMAT1 = tmpl.ParseBlock("FORMAT1"); //- 1: child block(ROW1) is not Out // Replace the label {THEAD1} in block FORMAT1 with OS. tmplFORMAT1.Assign("THEAD1", "OS"); // Replace the label {VALUE1} in block ROW1 with WinXP. tmplROW1.Assign("VALUE1", "WinXP"); // Be careful, the code below is commented deliberately. // So block ROW1 content won't be embeded into its parent block FORMAT1. // tmplROW1.Out(); tmplFORMAT1.Out(); //- 2: parent block(FORMAT1) is Out before child block(ROW1) // you won't see the ROW1 content in the result either. tmplFORMAT1.Out(); tmplROW1.Assign("VALUE1", "WinXP"); tmplROW1.Out(); //- 3: Replace the child block's label in the parent // block by outing child block first // It does work. tmplROW1.Out(); tmplROW1.Out(); tmplFORMAT1.Assign("THEAD1", "Tools"); // {VALUE1} is a label in block ROW1. tmplFORMAT1.Assign("VALUE1", "Visual Studio .NET"); //- 4: General usage ITmplBlock tmplROW2 = tmpl.ParseBlock("ROW2"); ITmplBlock tmplFORMAT2 = tmpl.ParseBlock("FORMAT2"); tmplFORMAT2.Assign("THEAD1", "Country"); tmplFORMAT2.Assign("THEAD2", "City"); tmplROW2.Assign("VALUE1", "China"); tmplROW2.Assign("VALUE2", "Pekin"); tmplROW2.Out(); tmplROW2.Assign("VALUE1", "China"); tmplROW2.Assign("VALUE2", "Shanghai"); tmplROW2.Out(); tmplROW2.Assign("VALUE2", "Tianjin"); tmplROW2.Out(); tmplROW2.Assign("VALUE2", "Chongqing"); tmplROW2.Out(); tmplROW2.Assign("VALUE2", "Shenzhen"); tmplROW2.Out(); tmplFORMAT2.Assign("VALUE1", "China"); tmplFORMAT2.Out(); // Calling the method ParseBlock, the one without // parameters, can get DOCUMENT-BLOCK. ITmplBlock tmplDoc = tmpl.ParseBlock(); tmplDoc.Out(); // the next step will usually be like the code below // which just sends the result content to the client. Response.Write(tmplDoc.BlockString);
然后
DOCUMENT-BLOCK
的结果内容如下所示<html> <head> <title> Example </title> </head> <body> <table> <!-- 1 --> <tr><th>OS</th></tr> <!-- 2 --> <tr><th>{THEAD1}</th></tr> <!-- 3 --> <tr><th>Tools</th></tr> <tr><td>Visual Studio .NET</td></tr> <tr><td>Visual Studio .NET</td></tr> <!-- 4 --> <tr><th>Country</th><th>City</th></tr> <tr><td>China</td><td>Pekin</td></tr> <tr><td>China</td><td>Shanghai</td></tr> <tr><td>China</td><td>Tianjin</td></tr> <tr><td>China</td><td>Chongqin</td></tr> <tr><td>China</td><td>Shenzhen</td></tr> </table> </body> </html>
结果内容中的注释是为了方便您与上面的 C# 代码进行对比而添加的。它们在实际结果内容中并不存在。正如大家所见,通过这种技术,演示代码 (HTML) 可以大量重用。
现在,我相信您已经对这项技术了解很多了,这真的会让我很高兴 :-)。有一点需要强调。那就是:如果一个块的 Out
方法没有被调用,它的内容将不会被放置在其父块中。
其次,我将通过一个简单的演示项目来说明如何使用代码。实际上,与其说它是使用说明,不如说它是一个展示模板解析器和池的框架。不过,我只会列出主要代码。详细内容请自行查阅源代码。所有源代码都可以通过上面的链接下载。
- 构建一个名为 Demo 的网站,其虚拟目录是您解压演示项目源文件的目录。下面我将使用 $DEMO 来指代这个虚拟目录。
- 在 IIS 中,禁用目录 $DEMO/tmpl(存放网页模板文件)和 $DEMO/src(存放源代码文件)的所有访问权限。这只是为了防止客户端以任何方式访问这两个目录下的任何资源。
- 查看配置文件 web.config。每个路径匹配 *do.aspx 的请求都将由
Demo.Handler.Controller
处理。$DEMO 是您网站虚拟目录的绝对路径。例如,如果您的虚拟目录是 E:/MyWebsite/demo,那么下面的配置应该是<add key="tmpldir" value="E:/MyWebsite/demo/tmpl/"/>
<configuration> <system.web> ... <httpHandlers> <add verb="*" path="*do.aspx" type="Demo.Handler.Controller, demo"/> </httpHandlers> ... </system.web> <appSettings> <add key="tmpldir" value="$DEMO/tmpl/"/> <add key="capacity" value="50"/> </appSettings> </configuration>
- 文件 Global.asax.cs 中的以下代码是使用单例模式创建一个
TemplatePool
的单一实例。public class Global : System.Web.HttpApplication { ... // // application's initialization // protected void Application_Start(Object sender, EventArgs e) { TemplatePool.Singleton( ConfigurationSettings.AppSettings.Get("tmpldir"), Int32.Parse(ConfigurationSettings.AppSettings.Get("capacity"))); } ... }
TemplatePool.Singleton
的作用如下所示。正如大家所见,它是线程安全的,并使用双重检查技巧来提高性能。并且池实例将在整个进程生命周期中存在。public sealed class TemplatePool : ITmplLoader { ... public static TemplatePool Singleton(string tmplDir, int capacity) { if(pool == null) { lock(objLock) { if(pool == null) pool = new TemplatePool(tmplDir, capacity); } } return pool; } ... private static TemplatePool pool = null; private static object objLock = new object(); }
- 查看类
Demo.Handler.Controller
,了解处理程序如何处理请求。public class Controller : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContext context) { ... // A more sophisticated way is to put the info, // such as Demo.Application.MenuDealer, menupanel.html etc, // into a config(an XML file or a table in db or others). IApplication theApp = (IApplication) Activator.CreateInstance(Type.GetType("Demo.Application.MenuDealer")); theApp.Init("menupanel.html", "login.html"); theApp.Session = context.Session; theApp.DoProcess(context.Request.Params); context.Response.Write(theApp.Out); } ... }
- 查看类
Demo.Application.MenuDealer
以了解DoProcess
方法的作用。它只是将数据与模板组合,生成一个要响应的字符串。public class MenuDealer : Dealer { ... // generate menus string oldModName = ""; string modname = null; string username = reqParams.Get("username"); Hashtable htMenu = null; ITmplBlock tmplMenuFrm = OutPageTmpl.ParseBlock("MENUFRM"); ITmplBlock tmplMenu = OutPageTmpl.ParseBlock("MENU"); int modIdx = 0; Operation operation = new Operation(); operation.GetMenuStart(); string action = "do.aspx?opname="; while((htMenu = operation.GetMenuNext()) != null) { modname = (string)htMenu["modname"]; if(!modname.Equals(oldModName)) { if(modIdx > 0) tmplMenuFrm.Out(); tmplMenuFrm.Assign("IDIDX", modIdx.ToString()); tmplMenuFrm.Assign("MODULE", modname); oldModName = modname; modIdx++; } string opname = (string)htMenu["opname"]; tmplMenu.Assign("REQUEST", opname + action + opname); tmplMenu.Assign("MENU", (string)htMenu["opvalue"]); tmplMenu.Out(); } if(modIdx > 0) tmplMenuFrm.Out(); tmplDoc = OutPageTmpl.ParseBlock(); tmplDoc.Assign("USERNAME", username); ... }
- 查看类
Demo.Application.Dealer
以了解属性Out
和OutPageTmpl
的作用。public abstract class Dealer : IApplication { ... public string Out { get { if(tmplDoc == null) return null; tmplDoc.Out(); return tmplDoc.BlockString; } } protected ITemplate GetTemplate(string tmplFileName) { return (ITemplate) (TemplatePool.Singleton().GetTemplate(tmplFileName)); } protected ITemplate OutPageTmpl { get { if(outPageTmpl == null) outPageTmpl = GetTemplate(outPage); return outPageTmpl; } } ... }
最后,使用 Visual Studio .NET 构建系统,您将看到上面图片所展示的内容。我相信您会在演示项目的源代码中找到更多有用的代码。正如我已经说过的,演示项目只是一个小型项目的框架。希望您会喜欢它!
关注点
- 当我实现
TemplatePool
类时,我需要一个用于链表的类。但是,我在命名空间System.Collections
中找不到一个。可能有人会大喊为什么不使用ArrayList
。好问题!但我猜ArrayList
在执行Insert
和Remove
操作时,会复制元素以移动它们的位置,这在 LRU 算法中频繁执行这两个操作时会带来性能损失。因此,我实现了一个简单的双向链表Agemo.Utility.DoubleLinkedList
来满足我的需求。它真的很简单。同样,越简单越好。 - 使用非递归DFS算法将模板分解成块。
- 如果
HttpHandler
类将使用会话对象,它必须继承接口IRequiresSessionState
,这只是一个标记接口。这个问题困扰了我很长时间。