Discuz!NT 模板机制分析

作为产品中的一大特色,模板机制一经推出,就引来了大家特别是站长们的关注。但它所饱受的风风 雨雨也成了那时不少人关注的话题。而今天本人将结合在产品组中的开发经历,介绍一下模板机制在设计 使用时的一些体会心得。希望借此陋文,使模板机制揭开“神秘”面纱,为大家在实际设计中提供一些有 价值的参考和建议。

    好了,开始今天的话题:)

    首先阐述一下模板设计的目标,因为这对于它最终要实现的功能非常重要。考虑到国内大部分站长基 本上都不具备.net开发背景,而我们的模板就是要降低这个门槛,便于站长进行设计订制以及修改等。而 另一个目的就是要提升aspx页面的访问速度,所以我们并未在模板设计时引入(web)控件机制,因为如果 使用.net控件,在windows的临时目录中会进行控件的订制生成(按用户设置的属性)。虽然在.net2.0 使用了fastobjectfactory的机制来提升页面生成的效率,比如使用batch批量编译选项 (web.config 文件中配置)生成的DLL(这里的DLL也是在临时目录下生成的随机命名的DLL文件,且重复编译的情况在所 难免)。但最终还是无法改变要生成服务器端控件的过程。   
    我们在设计模板本身所提供的语法时,尽可能逼近HTML的书写习惯,这样只要有HTML编写网页经验的 人就会很容易适应这种书写方式。当然有 asp开发经验的站长也能很快上手,因为模板的语法非常类似于 asp, 比如有<%if ...%>,<%else%>这样的写法等等。另外我们的模板语法也力求简练精悍,只需很少的 语法规则就直接支持生成内容丰富且形式多样的页面。说了这些,相信大家已经有兴趣来一看究竟了。不忙, 这里先要介绍一下如何使用模板机制来生成aspx页面。因为我有一位从事.net开发多年的朋友,在一次聊 天时他说,修改我们的前台页面时要手工修改"aspx/.../"下的相应的aspx文件,而当他看到 aspx文件中 的内容时大吃一惊,举个例子如下(aspx/1/logout.aspx):

.....命名空间和类的引用

 1<script runat="server">
2overrideprotectedvoid OnInit(EventArgs e)
3{
4
5 base.OnInit(e);
6
7 templateBuilder.Append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN
8   \" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n");
9 templateBuilder.Append("<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n");
10 templateBuilder.Append("<head>\r\n");
11 templateBuilder.Append("<meta http-equiv=\"Content-Type\" content=\"text/html;
12   charset=utf-8\" />\r\n");
13 templateBuilder.Append(""+ meta.ToString() +"\r\n");
14 templateBuilder.Append("<title>"+ pagetitle.ToString() +""+
15   config.Seotitle.ToString().Trim() +" - "+
16   config.Forumtitle.ToString().Trim() +" - Powered by Discuz!NT
17   </title>\r\n");
18 templateBuilder.Append("<link rel=\"icon\" href=\"favicon.ico\"
19   type=\"image/x-icon\"/>\r\n");
20 templateBuilder.Append("<link rel=\"shortcut icon\" href=\"favicon.ico\"
21   type=\"image/x-icon\"/>\r\n");
22 templateBuilder.Append("<!-- 调用样式表 -->\r\n");
23 templateBuilder.Append("<link rel=\"stylesheet\" href=\"templates/" +
24   templatepath.ToString() +"/dnt.css\"
25   type=\"text/css\" media=\"all\"  />\r\n");
26 templateBuilder.Append(""+ link.ToString() +"\r\n");
27 templateBuilder.Append("<script type=\"text/javascript\" src=\"templates/" +
28   templatepath.ToString() +"/report.js\"></" + "script>\r\n");
29 templateBuilder.Append("<script type=\"text/javascript\" src=\"templates/" +
30   templatepath.ToString() +"/common.js\"></" + "script>\r\n");
31 templateBuilder.Append("<script type=\"text/javascript\" src=\"editor/common.js\">
32   </" + "script>\r\n");
33 templateBuilder.Append("<script type=\"text/javascript\" src=\"editor/menu.js\">
34   </" + "script>\r\n");
35 templateBuilder.Append(""+ script.ToString() +"\r\n");
36 templateBuilder.Append("</head>\r\n");
37
38
39
 

    相信大家看到这样的aspx页面都会晕上一阵子,直接修改的想法已变得非常不现实了,简直是“不 可能完成的任务”。而实际上,我们并不希望大家或站长来完成这项工作。因为这是系统自动生成的。 而生成的前提就是在template/下的模板“目录”中的HTM文件。还是借用上面的logout,只是这里要看 的是模板目录下同名的logout.htm模板文件。它的内容如下:

1<%template _header%>
2<div id="foruminfo">
3<div class="userinfo">
4<h2><a href="{config.forumurl}">{config.forumtitle}</a><strong>用户退出</strong></h2>
5</div>
6</div>
7<!--TheCurrent end-->
8<%template _msgbox%>
9</div>
10<%template _footer%>

    大家可能会说,难道就是这几行就实现了上面aspx页面的内容吗?当然不是了,请大家注意:

    

1<%template _header%>

    这一行,其实就是告诉模板页面生成器: 这是一个子模板。

    因为我们在开始设计模板机制时就想到要简化模板代码并提升可重用性,因此要支持子模板机制。 这就类似于设计网页时的页首和页尾,我们在网页引用时,只需要include进来即可,而当修改页首和 页尾时,只须变动相应文件即可。

    这里不妨再打开_header.htm(注意子模板名称要用下划线开头),发现内容如下:

1<%template _pageheader%>
2<body>
3<div id="append_parent"></div>
4<div id="container">
5<!--header start-->
6<div id="header">
 

    有意思,又是一个“子模板”出现在了第一行。不错,我们的机制允许模板被嵌套使用,这样会 使页面的“组装”更加灵活多样。

    即然都走到这一步,不妨再打开_pageheader子模板,正所谓“不撞南墙不回头”嘛:)

  1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR
2/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml">
4<head>
5<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6{meta}
7<title>{pagetitle}{config.seotitle}-{config.forumtitle}- Powered by Discuz!NT</title>
8<link rel="icon" href="favicon.ico" type="image/x-icon"/>
9<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
10<!-- 调用样式表 -->
11<link rel="stylesheet" href="templates/{templatepath}/dnt.css" type="text/css" media="all"/>
12{link}
13<script type="text/javascript" src="templates/{templatepath}/report.js"></script>
14<script type="text/javascript" src="templates/{templatepath}/common.js"></script>
15<script type="text/javascript" src="editor/common.js"></script>
16<script type="text/javascript" src="editor/menu.js"></script>
17{script}
18</head>

    折腾了一圈,到这里出现了上面aspx页中的对应内容,有意思吧,不过里面的{pagetitle}和{ config.seotitle}以及{config.forumtitle}这样的东东又是什么呢? 其实非常简单,这就是按照模 板语法格式所书写的代码,因为这两处在模板生成之后会变成

  

1     templateBuilder.Append("<title>"+ pagetitle.ToString() +""+
2   config.Seotitle.ToString().Trim() +" - "+
3   config.Forumtitle.ToString().Trim() +" - Powered by Discuz!NT
4   </title>\r\n"); 

    好了,到了这里我们应该清楚了,以后要修改前台页面的一个标准流程:

    1.按模板语法修改相应的模板文件夹下的模板文件;     2.在后台生成或使用官方的模板生成器生成相应aspx页面即可;         其实流程非常简单,相信即使不懂aspx开发的朋友也会很快适应并上手。前提就是要了解模板语 法,除了上面所说的以外,还有一些常用的语法如下图:

            这里不妨引用官方文档中的链接,里面的说明会更清楚:)

    相关链接如下:http://nt.discuz.net/download/doc/dnt_2_skindoc.zip

    好了,目前我们只是知道了如使使用和修改它,但所谓的“模板生成”机制又是个什么样子呢! 必定到这里我们只走完了一半旅途,下面将会介绍模板的生成机制。

    首先要看一下后台的模板(列表)管理界面,如下图:

    从上图可知道,模板是按名称(目录)来进行管理的,而每个模板都有名称,存放路径,版权, 作者等相关信息。而这此信息都是来自于每个模板(目录)下的about.xml文件,这里将它的内容贴 出来:

 1<?xml version="1.0" encoding="utf-8"?>
2<about>
3     <template name="basic"
4          author="Discuz!NT"
5          createdate ="2007-11-12"
6          ver="1.1112"
7          fordntver="2.0"
8          copyright="Copyright 2007 Comsenz Inc."/>
9</about>

    注: 上图中的那个“乐队演出”图片其实是模板目录下的about.png文件,它相当于一张预览图。

    需要说明的是上图中不是所有模板都能在前台使用,而是当被标记为“已入库”才可在前台使用, 而入库即数据库,下面就是数据库中的截图:

             而接下来要说的,就是模板列表中每个模板后面的“生成”链接所要干的活了。

    如果大家手头上有reflector的话,请使用这个工具加载我们官方提供的产品目录下的bin文件夹 中的discuz.common.dll文件,找到 PageTemplate这个类。这里为了便于说明,将反射所得到的代码 加上注释贴出来:

 1publicabstractclass PageTemplate
  2 {
  3  publicstatic Regex[] r =new Regex[21];
  4
  5  static PageTemplate()
  6  {
  7
  8                        RegexOptions options = Utils.GetRegexCompiledOptions();
  9
10   r[0] =new Regex(@"<%template ([^\[\]\{\}\s]+)%>", options);
11
12   r[1] =new Regex(@"<%loop ((\(([a-zA-Z]+)\) )?)([^\[\]\{\}\s]+) ([^\[\]\{\}\s]+)%>", options);
13
14   r[2] =new Regex(@"<%\/loop%>", options);
15
16   r[3] =new Regex(@"<%while ([^\[\]\{\}\s]+)%>", options);
17
18   r[4] =new Regex(@"<%\/while ([^\[\]\{\}\s]+)%>", options);
19
20   r[5] =new Regex(@"<%if (?:\s*)(([^\s]+)((?:\s*)(\|\||\&\&)(?:\s*)([^\s]+))?)(?:\s*)%>", options);
21
22   r[6] =new Regex(@"<%else(( (?:\s*)if (?:\s*)(([^\s]+)((?:\s*)(\|\||\&\&)(?:\s*)([^\s]+))?))?)(?:\s*)%>", options);
23
24   r[7] =new Regex(@"<%\/if%>", options);
25
26   //解析{var.a}
27   r[8] =new Regex(@"(\{strtoint\(([^\s]+?)\)\})", options);
28
29   //解析{request[a]}
30   r[9] =new Regex(@"(<%urlencode\(([^\s]+?)\)%>)", options);
31
32   //解析{var[a]}
33   r[10] =new Regex(@"(<%datetostr\(([^\s]+?),(.*?)\)%>)", options);
34   r[11] =new Regex(@"(\{([^\.\[\]\{\}\s]+)\.([^\[\]\{\}\s]+)\})", options);
35
36   //解析普通变量{}
37   r[12] =new Regex(@"(\{request\[([^\[\]\{\}\s]+)\]\})", options);
38
39   //解析==表达式
40   r[13] =new Regex(@"(\{([^\[\]\{\}\s]+)\[([^\[\]\{\}\s]+)\]\})", options);
41
42   //解析==表达式
43   r[14] =new Regex(@"({([^\[\]/\{\}='\s]+)})", options);
44
45   //解析普通变量{}
46   r[15] =new Regex(@"({([^\[\]/\{\}='\s]+)})", options);
47
48   //解析==表达式
49   r[16] =new Regex(@"(([=|>|<|!]=)\\"+"\"" + @"([^\s]*)\\" + "\")", options);
50  
51   //命名空间
52   r[17] =new Regex(@"<%namespace ([^\[\]\{\}\s]+)%>", options);
53  
54   //C#代码
55   r[18] =new Regex(@"<%csharp%>([\s\S]+?)<%/csharp%>", options);
56
57   //set标签
58   r[19] =new Regex(@"<%set ((\(([a-zA-Z]+)\))?)(?:\s*)\{([^\s]+)\}(?:\s*)=(?:\s*)(.*?)(?:\s*)%>", options);
59
60                        r[20] =new Regex(@"(<%getsubstring\(([^\s]+?),(.\d*?),(.\d*?),([^\s]+?)\)%>)", options);
61  }

62
63
64  ///<summary>
65  /// 获得模板字符串. 首先查找缓存. 如果不在缓存中则从设置中的模板路径来读取模板文件.
66  /// 模板文件的路径在Web.config文件中设置.
67  /// 如果读取文件成功则会将内容放于缓存中.
68  ///</summary>
69  ///<param name="skinName">模板名</param>
70  ///<param name="templateName">模板文件的文件名称, 也是缓存中的模板名称.</param>
71  ///<param name="nest">嵌套次数</param>
72  ///<param name="templateid">模板id</param>
73  ///<returns>string值,如果失败则为"",成功则为模板内容的string</returns>

74  publicvirtualstring GetTemplate(string forumpath,string skinName, string templateName, int nest,int templateid)
75  {
76   StringBuilder strReturn =new StringBuilder();
77   if (nest <1)
78   {
79    nest =1;
80   }

81   elseif (nest >5)
82   {
83    return"";
84   }

85
86
87   string extNamespace ="";
88   string pathFormatStr ="{0}{1}{2}{3}{4}.htm";
89                        string filePath =string.Format(pathFormatStr, Utils.GetMapPath(forumpath +"templates"), System.IO.Path.DirectorySeparatorChar, skinName, System.IO.Path.DirectorySeparatorChar, templateName);
90  
91   //如果指定风格的模板文件不存在
92   if (!System.IO.File.Exists(filePath))
93   {
94    //默认风格的模板是否存在
95                                filePath =string.Format(pathFormatStr, Utils.GetMapPath(forumpath +"templates"), System.IO.Path.DirectorySeparatorChar, "default", System.IO.Path.DirectorySeparatorChar, templateName);
96    if (!System.IO.File.Exists(filePath))
97    {
98     return"";
99    }

100   }

101   using(System.IO.StreamReader objReader =new System.IO.StreamReader(filePath, Encoding.UTF8))
102   {
103    System.Text.StringBuilder textOutput =new System.Text.StringBuilder();
104   
105    textOutput.Append(objReader.ReadToEnd());
106    objReader.Close();
107
108    //处理命名空间
109    if (nest ==1)
110    {
111     //命名空间
112     foreach (Match m in r[17].Matches(textOutput.ToString()))
113     {
114      extNamespace +="\r\n<%@ Import namespace=\"" + m.Groups[1].ToString() + "\" %>";
115      textOutput.Replace(m.Groups[0].ToString(), string.Empty);
116     }

117
118    }

119    //处理Csharp语句
120    foreach (Match m in r[18].Matches(textOutput.ToString()))
121    {
122     //csharpCode += "\r\n" + m.Groups[1].ToString() + "\r\n";
123     textOutput.Replace(m.Groups[0].ToString(), m.Groups[0].ToString().Replace("\r\n", "\r\t\r"));
124    }

125
126    textOutput.Replace("\r\n", "\r\r\r");
127    textOutput.Replace("<%", "\r\r\n<%");
128    textOutput.Replace("%>", "%>\r\r\n");
129
130    textOutput.Replace("<%csharp%>\r\r\n", "<%csharp%>").Replace("\r\r\n<%/csharp%>", "<%/csharp%>");
131   
132
133    string[] strlist = Utils.SplitString(textOutput.ToString(), "\r\r\n");
134    int count = strlist.GetUpperBound(0);
135 
136    for (int i =0; i <= count; i++)
137    {
138     strReturn.Append(ConvertTags(nest,forumpath, skinName, strlist[i], templateid));
139    }

140   }

141   if (nest ==1)
142   {
143                                string template =string.Format("<%@ Page language=\"c#\" Codebehind=\"{0}.aspx.cs\" AutoEventWireup=\"false\" EnableViewState=\"false\" Inherits=\"Discuz.ForumPage.{0}\" %>\r\n<%@ Import namespace=\"System.Data\" %>\r\n<%@ Import namespace=\"Discuz.Common\" %>\r\n<%@ Import namespace=\"Discuz.Forum\" %>\r\n<%@ Import namespace=\"Discuz.Entity\" %>\r\n{1}\r\n<script runat=\"server\">\r\noverride protected void OnInit(EventArgs e)\r\n{{\r\n\r\n\t/* \r\n\t\tThis page was created by Discuz!NT Template Engine at {2}.\r\n\t\t本页面代码由Discuz!NT模板引擎生成于 {2}. \r\n\t*/\r\n\r\n\tbase.OnInit(e);\r\n{3}\r\n\tResponse.Write(templateBuilder.ToString());\r\n}}\r\n</script>\r\n", templateName, extNamespace, DateTime.Now.ToString(), strReturn.ToString());
144
145    string pageDir = Utils.GetMapPath(forumpath +"aspx\\"+ templateid.ToString() +"\\");
146    if (!Directory.Exists(pageDir))
147    {
148     Utils.CreateDir(pageDir);
149    }

150
151    string outputPath = pageDir  + templateName +".aspx";
152   
153    
154   
155    using (FileStream fs =new FileStream(outputPath, FileMode.Create,FileAccess.ReadWrite, FileShare.ReadWrite))
156    {
157     Byte[] info = System.Text.Encoding.UTF8.GetBytes(template);
158     fs.Write(info, 0, info.Length);
159     fs.Close();
160    }

161   
162   }

163   return strReturn.ToString();
164  }

165 
166  ///<summary>
167  /// 转换标签
168  ///</summary>
169  ///<param name="nest">深度</param>
170  ///<param name="skinName">模板名称</param>
171  ///<param name="inputStr">模板内容</param>
172  ///<param name="templateid">模板id</param>
173  ///<returns></returns>

174  privatestring ConvertTags(int nest,string forumpath, string skinName, string inputStr, int templateid)
175  {
176   string strReturn ="";
177   bool IsCodeLine;
178   string strTemplate;
179   strTemplate = inputStr.Replace("\\", "\\\\");
180   strTemplate = strTemplate.Replace("\"", "\\\"");
181   strTemplate = strTemplate.Replace("</script>", "</\"+ \"script>");
182   IsCodeLine =false;
183 
184
185   foreach (Match m in r[0].Matches(strTemplate))
186   {
187    IsCodeLine =true;
188                                strTemplate = strTemplate.Replace(m.Groups[0].ToString(), "\r\n"+ GetTemplate(forumpath,skinName, m.Groups[1].ToString(), nest +1, templateid) +"\r\n");
189   }

190
191   foreach (Match m in r[1].Matches(strTemplate))
192   {
193    IsCodeLine =true;
194    if (m.Groups[3].ToString() =="")
195    {
196     strTemplate = strTemplate.Replace(m.Groups[0].ToString(),
197      string.Format("\r\n\tint {0}__loop__id=0;\r\n\tforeach(DataRow {0} in {1}.Rows)\r\n\t{{\r\n\t\t{0}__loop__id++;\r\n", m.Groups[4].ToString(), m.Groups[5].ToString()));
198    }

199    else
200    {
201     strTemplate = strTemplate.Replace(m.Groups[0].ToString(),
202      string.Format("\r\n\tint {1}__loop__id=0;\r\n\tforeach({0} {1} in {2})\r\n\t{{\r\n\t\t{1}__loop__id++;\r\n", m.Groups[3].ToString(), m.Groups[4].ToString(), m.Groups[5].ToString()));
203    }

204   }

205
206  
207
208  
209
210   
211   if (IsCodeLine)
212   {
213    strReturn = strTemplate +"\r\n";
214   }

215   else
216   {
217    if (strTemplate.Trim() !="")
218    {
219     StringBuilder sb =new StringBuilder();
220     foreach (string temp in Utils.SplitString(strTemplate,"\r\r\r"))
221     {
222      if (temp.Trim() =="")
223       continue;
224      sb.Append("\ttemplateBuilder.Append(\"" + temp + "\\r\\n\");\r\n");
225     }

226     strReturn = sb.ToString();
227    }

228   }

229   return strReturn;
230  }

231
232
233
234  ///<summary>
235  /// 解析特殊变量
236  ///</summary>
237  ///<returns></returns>

238  publicabstractstring ReplaceSpecialTemplate(string forumpath,string skinName,string strTemplate);
239 }

    基本上都是对正则式的使用,因为本人不是这方面的高手,所以就不多说了,相信开源之后大家拿 源码和注释一看便知:)

    这里需要说明的就是ReplaceSpecialTemplate(string forumpath,string skinName,....) 这个函 数,它的实现我们要到discuz.forum.dll中去找,这里为了方便,直接就将反射出来的代码加上注释贴 出来,大家一看便知:

 publicclass ForumPageTemplate : PageTemplate
2{
3
4 ///<summary>
5 /// 解析特殊变量
6 ///</summary>
7 ///<param name="skinName">皮肤名</param>
8 ///<param name="strTemplate">模板内容</param>
9 ///<returns></returns>

10publicoverridestring ReplaceSpecialTemplate(string forumpath,string skinName,string strTemplate)
11 {
12  Regex r;
13  Match m;
14 
15  StringBuilder sb =new StringBuilder();
16  sb.Append(strTemplate);
17      r =new Regex(@"({([^\[\]/\{\}='\s]+)})", RegexOptions.IgnoreCase|RegexOptions.Multiline|RegexOptions.Compiled);
18  for (m = r.Match(strTemplate); m.Success; m = m.NextMatch())
19  {
20   if (m.Groups[0].ToString() =="{forumversion}")
21   {
22    sb = sb.Replace(m.Groups[0].ToString(), Utils.GetAssemblyVersion());
23   }

24   elseif (m.Groups[0].ToString() =="{forumproductname}")
25   {
26    sb = sb.Replace(m.Groups[0].ToString(), Utils.GetAssemblyProductName());
27   }

28  }

29
30  foreach(DataRow dr in GetTemplateVarList(forumpath,skinName).Rows)
31  {
32   sb = sb.Replace(dr["variablename"].ToString().Trim(), dr["variablevalue"].ToString().Trim());
33  }

34  return sb.ToString();
35 }

36
37
38 ///<summary>
39 /// 获取模板内容
40 ///</summary>
41 ///<param name="skinName">皮肤名</param>
42 ///<param name="templateName">模板名</param>
43 ///<param name="nest">嵌套次数</param>
44 ///<param name="templateid">皮肤id</param>
45 ///<returns></returns>

46publicoverridestring GetTemplate(string forumpath,string skinName, string templateName, int nest,int templateid)
47 {
48  returnbase.GetTemplate(forumpath,skinName,templateName,nest,templateid);
49 }

50
51 ///<summary>
52 /// 获得模板变量列表
53 ///</summary>
54 ///<param name="skinName">皮肤名</param>
55 ///<returns></returns>

56publicstatic DataTable GetTemplateVarList(string forumpath,string skinName)
57 {
58  Discuz.Cache.DNTCache cache = Discuz.Cache.DNTCache.GetCacheService();
59                DataTable dt = cache.RetrieveSingleObject("/Forum/"+ skinName +"/TemplateVariable") as DataTable;
60
61  if(dt !=null)
62  {
63   return dt;
64  }

65  else
66  {
67   DataSet dsSrc =new DataSet("template");
68   string[] filename =newstring[1] {Utils.GetMapPath(forumpath +"templates/"+ skinName +"/templatevariable.xml")};
69   
70   if (Utils.FileExists(filename[0]))
71   {
72         dsSrc.ReadXml(filename[0]);
73
74                              if (dsSrc.Tables.Count ==0)
75                              {
76                                
77                              }

78   }

79   else
80   {
81      
82   }

83
84                        cache.AddSingleObject("/Forum/"+ skinName +"/TemplateVariable", dsSrc.Tables[0], filename);
85   return dsSrc.Tables[0];
86  }

87 }

88}

    相信看到这里,熟悉设计模式的朋友会看出来,这里用到了"Template Method"模式,因为这 种模式很简单,就不多做介绍了,相关信息可以看一下GOF的那本书或到网上一搜便知。

    下面要说的就是上面的这个 ForumPageTemplate类目前所要实现的功能。因为模板中要被订制 的东西有很多,而我们目前所搭建的功能只是为了生成和转换时使用,当用户有要替换的特殊变量 就会出现无法订制的情况。所以才提供了这个类以便实现与模板有关的用户订制需求。当然目录所 提供的功能只是简单的替换而已,但并不排除以后随着用户口味的挑剔而进行升级扩展的可能。

    而用户进行特殊变量定制也非常简单,只要在上面所贴的后台“模板列表”图中的后面点击相 应的“管理”链接之后就会看到下面的页面,如图:

        只要再点击右下方的“模板变量列表”,即可以进入定制模板变量的页面,如图:

        

大家只要进行相应操作设置即可。

转自:http://www.cnblogs.com/daizhj/archive/2007/12/17/1003150.html

原文地址:https://www.cnblogs.com/suzh/p/2444261.html