65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (4投票s)

2004 年 8 月 17 日

6分钟阅读

viewsIcon

64584

downloadIcon

1556

一个简单的网页模板解析器,用于将布局与代码分离。

Sample Image

引言

ASP.NET 为我们提供了许多构建 Web 系统的简单方法,尤其是代码隐藏技术,它令人惊叹地实现了布局和代码的分离。然而,ASP.NET 也提供了一些机制,允许您构建比代码隐藏提供的功能更丰富的自定义编程模型。其中一种机制是 HTTP Handler,它提供了一种与 IIS Web 服务器的底层请求和响应服务交互的方式,并提供了类似于 ISAPI 扩展的功能,但编程模型更简单。太棒了!这种机制正是我最喜欢的,因为它给我一种一切尽在掌控之中,并且我自由自在的感觉。

但是当你编写自定义 HTTP 处理程序时,硬编码页面布局是枯燥且容易出错的。我们确实需要一种将布局与代码分离的方法。因此,TmplParser 类、TemplatePool 类等应运而生。TmplParser 类主要用于解析带有我自己定义的简单标签和标记的布局模板文件 :-)。TemplatePool 类用于缓冲一组模板,这可以减少 I/O 操作(即减少模板文件读取),从而提高性能。稍后我将在本文中演示如何使用它们来分离布局与代码。

注意:这是我第一次使用 ASP.NET 编程,第一次使用 C# 编程,第一次接触 IIS,也是第一次向 Code Project 提交文章。因此,文章和代码中肯定存在一些问题或不足之处。我非常欢迎任何反馈。谢谢!

使用代码

首先,我将向您介绍解析器的使用规则和一些基本信息。

  1. 模板文件只包含两个基本元素:blocklabel。一个块由开始标记 <!--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 中,有四个块:FORMAT1ROW1FORMAT2ROW2。块 FORMAT1 有一个标签:THEAD1;块 ROW1 有一个标签:VALUE1;块 FORMAT2 有两个标签:THEAD1THEAD2;块 ROW2 有两个标签:VALUE1VALUE2FORMAT1FORMAT2 分别是 ROW1ROW2 的父块。实际上,还有一个块。它是 root block,即模板文件本身。我们称之为 DOCUMENT-BLOCK

  2. 模板文件定义规则
    • 块名在一个模板文件中必须是唯一的。这不仅是为了代码可读性,也是为了便于使用。我的原则是:越简单越好。
    • 一个块可以通过包含其他块而成为父块。但任何两个块都不能重叠。并且没有兄弟关系,只维护块之间的父子关系。
  3. 深入探究

    当模板文件被 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 方法没有被调用,它的内容将不会被放置在其父块中。

其次,我将通过一个简单的演示项目来说明如何使用代码。实际上,与其说它是使用说明,不如说它是一个展示模板解析器和池的框架。不过,我只会列出主要代码。详细内容请自行查阅源代码。所有源代码都可以通过上面的链接下载。

  1. 构建一个名为 Demo 的网站,其虚拟目录是您解压演示项目源文件的目录。下面我将使用 $DEMO 来指代这个虚拟目录。
  2. 在 IIS 中,禁用目录 $DEMO/tmpl(存放网页模板文件)和 $DEMO/src(存放源代码文件)的所有访问权限。这只是为了防止客户端以任何方式访问这两个目录下的任何资源。
  3. 查看配置文件 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>
  4. 文件 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();
    }
  5. 查看类 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);
     }
     ...
    }
  6. 查看类 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);
        ...
    }
  7. 查看类 Demo.Application.Dealer 以了解属性 OutOutPageTmpl 的作用。
    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 构建系统,您将看到上面图片所展示的内容。我相信您会在演示项目的源代码中找到更多有用的代码。正如我已经说过的,演示项目只是一个小型项目的框架。希望您会喜欢它!

关注点

  1. 当我实现 TemplatePool 类时,我需要一个用于链表的类。但是,我在命名空间 System.Collections 中找不到一个。可能有人会大喊为什么不使用 ArrayList。好问题!但我猜 ArrayList 在执行 InsertRemove 操作时,会复制元素以移动它们的位置,这在 LRU 算法中频繁执行这两个操作时会带来性能损失。因此,我实现了一个简单的双向链表 Agemo.Utility.DoubleLinkedList 来满足我的需求。它真的很简单。同样,越简单越好。
  2. 使用非递归DFS算法将模板分解成块。
  3. 如果 HttpHandler 类将使用会话对象,它必须继承接口 IRequiresSessionState,这只是一个标记接口。这个问题困扰了我很长时间。
© . All rights reserved.