dasBlog的模板引擎(二)----内部实现

    在上一篇文章中介绍了dasBlog模板引擎的两个概念Theme和Macro。这一篇文章介绍dasBlog模板引擎的运行过程。
    先简单概括一下dasBlog模板引擎的原理。它提供了一个ShareBasePage的页基类,所有的页面类都从这个类派生,在这个类里,进行页面状态的传递和最终页面的生成。可以说,这是整个模板引擎的一个全局控制者。生成的方法是:在ShareBasePage里读取相应的template文件,顺序提取其中的HTML代码和宏进行相应的处理后添加到页面的PlaceHolder中。HTML代码是被添加到Literal后添加,取得宏表达式后,会转换成相应的Control后再添加到PlaceHolder。这就是页面生成的整个过程。

      我们先来看看SharedBasePage的构造函数。从里面两个函数名就可以看出,进行的是状态加载和页面初始化的任务。

None.gif          public  SharedBasePage()
ExpandedBlockStart.gifContractedBlock.gif        
dot.gif {
InBlock.gif                          
InBlock.gif            Init 
+= new EventHandler(this.SessionLoad);
InBlock.gif            Init 
+= new EventHandler(this.SetupPage);
InBlock.gif            
ExpandedBlockEnd.gif        }

      SharedBasePager中的Property添加了自定义的PersistentPageStateAttribute属性之后,每次页面加载完成,该属性都会被保存在Cookie中,第一次开始加载的时候又会被读出来。这个读取来的工作就在SessionLoad里完成了,而保存的工作就在SessionStore里完成了。
而SetupPage做的是些全球和参数处理方面的操作。在SetupPage中还完成了一个很重要的工作,加载了Macros类,这个类里包含了一些全局宏。所谓全局宏是指:网站名称,Trackback排行榜等在任何一个页面都会被显示的内容。相对的,有继承自Macros类的DayMacros(显示某日文章列表的页面的宏的集合),ItemMacros(显示单篇文章的页面宏的集合),EditMacros(文章编辑页面的宏的集合)。

     当SharedBasePage初始化完成之后,就开始正式进入页面生成步骤了。在SharedBasePage里ProcessTemplate方法是做这个工作的。以下是它的源码:

None.gif          public   virtual   void  ProcessTemplate()
ExpandedBlockStart.gifContractedBlock.gif        
dot.gif {
InBlock.gif            TemplateProcessor templateProcessor 
= new TemplateProcessor();
InBlock.gif            
string path = Request.PhysicalApplicationPath;
                 //取得当前使用Template对应的文件内容
InBlock.gif            
string templateString = GetPageTemplate(path);
InBlock.gif            
InBlock.gif            Match match 
= findBodyTag.Match(templateString);
InBlock.gif            
if ( match.Success )
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
// this section splits the template into a header, body and footer section
InBlock.gif                
// (all above and including <body>, everything between <body></body> and all below and including </body>
InBlock.gif                
//找到<body>的位置
InBlock.gif
                int indexBody = templateString.IndexOf("</body>");
InBlock.gif                
if ( indexBody == -1 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    indexBody 
= templateString.IndexOf("</BODY>");
ExpandedSubBlockEnd.gif                }

InBlock.gif
InBlock.gif                
//取得<head>部分
InBlock.gif                
// the header template contains everything above and including the body tag
InBlock.gif
                string headerTemplate=templateString.Substring(0,match.Index+match.Length);
InBlock.gif                
InBlock.gif                
// insert necessary headtags and fix stylesheet relative links
InBlock.gif                
// fix any relative css link tags
InBlock.gif                
//转换地址
ExpandedSubBlockStart.gifContractedSubBlock.gif
                headerTemplate = hrefRegEx.Replace(headerTemplate, String.Format("href=\"dot.gif{0}themes", Utils.GetBaseUrl()));
InBlock.gif

ExpandedSubBlockStart.gifContractedSubBlock.gif                
string baseTag = String.Format("<base href=\"dot.gif{0}\"></base>\r\n", Utils.GetBaseUrl());
ExpandedSubBlockStart.gifContractedSubBlock.gif                
string linkTag = String.Format("<link rel=\"alternate\" type=\"application/rss+xml\" title=\"dot.gif{2}\" href=\"dot.gif{0}\" />\r\n<link rel=\"alternate\" type=\"application/atom+xml\" title=\"dot.gif{2}\" href=\"dot.gif{1}\" />\r\n", Utils.GetRssUrl(),Utils.GetAtomUrl(),HttpUtility.HtmlEncode(siteConfig.Title));
InBlock.gif
InBlock.gif                
int indexHead = headerTemplate.IndexOf("</head>");
InBlock.gif                
if ( indexHead == -1 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    indexHead 
= headerTemplate.IndexOf("</HEAD>");
ExpandedSubBlockEnd.gif                }

InBlock.gif                // 给<head>部分添加css等
InBlock.gif                headerTemplate = headerTemplate.Insert(indexHead, baseTag.ToString() + linkTag.ToString());
InBlock.gif
InBlock.gif                
// therefore it must close with a closing angle bracket, but it's better to check 
InBlock.gif
                if ( headerTemplate[headerTemplate.Length-1== '>' )
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    
// if that's so, we want to inject the reading order designator if we're right-to-left
InBlock.gif                    
// or it's explicitly specified
InBlock.gif
                    string pageReadingDirection = coreStringTables.GetString("page_reading_direction");
InBlock.gif                    
if ( pageReadingDirection != null && pageReadingDirection.Length > 0 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        
if (pageReadingDirection == "RTL"this.readingDirection = TextDirection.RightToLeft; 
InBlock.gif                        headerTemplate 
= headerTemplate.Substring(0, headerTemplate.Length-1+ " dir=\"" + pageReadingDirection + "\">";
ExpandedSubBlockEnd.gif                    }

ExpandedSubBlockEnd.gif                }

InBlock.gif
InBlock.gif                
string bodyTemplate,footerTemplate;
InBlock.gif                
if( indexBody != -1 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
                        //取得template中的主体部分
InBlock.gif                    bodyTemplate
=templateString.Substring(match.Index+match.Length,indexBody-(match.Index+match.Length));
                        // 取得template中的footer部分
InBlock.gif                    footerTemplate
=templateString.Substring(indexBody);
ExpandedSubBlockEnd.gif                }

InBlock.gif                
else
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    bodyTemplate
=templateString.Substring(match.Index+match.Length);
InBlock.gif                    footerTemplate
="";
ExpandedSubBlockEnd.gif                }

InBlock.gif                        
InBlock.gif                
// 对headerTemplate进行宏处理,添加到页面的PlaceHolder中
InBlock.gif
                templateProcessor.ProcessTemplate( this, headerTemplate, ContentPlaceHolder, macros );
InBlock.gif                
                  // 初始化一个Form
InBlock.gif
                BaseHtmlForm mainForm = new BaseHtmlForm();
InBlock.gif                mainForm.ID 
= "mainForm";
                  // 将mainForm添加到PlaceHolder中
InBlock.gif                ContentPlaceHolder.Controls.Add(mainForm);
InBlock.gif                
// 对bodyTemplate进行宏处理,添加到mainForm中
InBlock.gif
                templateProcessor.ProcessTemplate( this, bodyTemplate, mainForm, macros );
InBlock.gif               // 对footerTemplate进行宏处理,添加到页面的PlaceHolder中

InBlock.gif
                if ( footerTemplate.Length > 0 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    templateProcessor.ProcessTemplate( 
this, footerTemplate, ContentPlaceHolder, macros );
ExpandedSubBlockEnd.gif                }

ExpandedSubBlockEnd.gif            }

InBlock.gif            
else
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
// if the page is just an unrecognizable mess of tags, process in one shot.
InBlock.gif
                templateProcessor.ProcessTemplate( this, templateString, ContentPlaceHolder, macros );
ExpandedSubBlockEnd.gif            }

InBlock.gif            
ExpandedBlockEnd.gif        }

在这个方法里,只是进行了整个生成流程的控制,第一步工作当然是取得模板文件了。

None.gif string  templateString  =  GetPageTemplate(path);

模板文件的获取是在Theme类里面完成了的。Theme里的OpenTemplate()函数返回一个TextReader对象,在SharedBasePage里读成string,页面生成的过程就是对这个string进行操作了。而真正生成页面的工作是在TemplateProcessor这个类里完成的。而TemplateProcessor里起主要作用的其实也只是一个ProcessTemplate方法,其代码如下:

None.gif          public   void  ProcessTemplate(SharedBasePage page, Entry entry,  string  templateString, Control contentPlaceHolder, Macros macros)
ExpandedBlockStart.gifContractedBlock.gif        
dot.gif {
InBlock.gif            
int lastIndex = 0;
InBlock.gif
InBlock.gif            MatchCollection matches 
= templateFinder.Matches(templateString);
InBlock.gif            
foreach( Match match in matches )
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
//添加宏之外的其它字符串
InBlock.gif
                if ( match.Index > lastIndex )
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    contentPlaceHolder.Controls.Add(
new LiteralControl(templateString.Substring(lastIndex,match.Index-lastIndex)));
ExpandedSubBlockEnd.gif                }

InBlock.gif                Group g 
= match.Groups["macro"];
InBlock.gif                Capture c 
= g.Captures[0];
InBlock.gif                
InBlock.gif                Control ctrl 
= null;
InBlock.gif                
object targetMacroObj = macros;
InBlock.gif                
string captureValue = c.Value;
InBlock.gif                
InBlock.gif                
//Check for a string like: <%foo("bar", "bar")|assemblyConfigName%>
InBlock.gif
                int assemblyNameIndex = captureValue.IndexOf(")|");
InBlock.gif                
//是否是自定义宏
InBlock.gif
                if (assemblyNameIndex != -1//use the default Macros
ExpandedSubBlockStart.gifContractedSubBlock.gif
                dot.gif{
InBlock.gif                    
//The QN minus the )| 
InBlock.gif                    
//取得自定义assemblyName
InBlock.gif
                    string macroAssemblyName = captureValue.Substring(assemblyNameIndex+2);
InBlock.gif                    
//The method, including the )
InBlock.gif                    
//取得宏名称
InBlock.gif
                    captureValue = captureValue.Substring(0,assemblyNameIndex+1);
InBlock.gif
InBlock.gif                    
//生成自定义宏对象
InBlock.gif
                    try
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        targetMacroObj 
= MacrosFactory.CreateCustomMacrosInstance(page, entry, macroAssemblyName);
ExpandedSubBlockEnd.gif                    }

InBlock.gif                    
catch (Exception ex)
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        page.LoggingService.AddEvent(
new EventDataItem(EventCodes.Error,String.Format("Error executing Macro: {0}",ex.ToString()),string.Empty));
ExpandedSubBlockEnd.gif                    }

ExpandedSubBlockEnd.gif                }

InBlock.gif
InBlock.gif                
try
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    
//执行宏,返回一个对象
InBlock.gif
                    ctrl = InvokeMacro(targetMacroObj,captureValue) as Control;
InBlock.gif                    
if (ctrl != null)
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        contentPlaceHolder.Controls.Add(ctrl);
ExpandedSubBlockEnd.gif                    }

InBlock.gif                    
else 
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        page.LoggingService.AddEvent(
new EventDataItem(EventCodes.Error,String.Format("Error executing Macro: {0} returned null.",captureValue),string.Empty));
ExpandedSubBlockEnd.gif                    }

ExpandedSubBlockEnd.gif                }

InBlock.gif                
catch (Exception ex)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    
string error = String.Format("Error executing macro: {0}. Make sure it you're calling it in your BlogTemplate with paratheses like 'myMacro()'. Macros with parameter lists and overloads must be called in this way. Exception: {1}",c.Value, ex.ToString());
InBlock.gif                    page.LoggingService.AddEvent(
new EventDataItem(EventCodes.Error,error,string.Empty));
ExpandedSubBlockEnd.gif                }

InBlock.gif                lastIndex 
= match.Index+match.Length;
ExpandedSubBlockEnd.gif            }

InBlock.gif            
if ( lastIndex < templateString.Length)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
//将宏添加到PlacheHolder中
InBlock.gif
                contentPlaceHolder.Controls.Add(new LiteralControl(templateString.Substring(lastIndex,templateString.Length-lastIndex)));
ExpandedSubBlockEnd.gif            }

ExpandedBlockEnd.gif        }

这里的做的工作就是匹配所有类似<%SiteName%>这样的字符串,把它转化成相应的Control,我把它叫做“执行宏”,执行宏的工作在这里进行:

None.gif ctrl  =  InvokeMacro(targetMacroObj,captureValue)  as  Control;

以下是InvodeMacro代码:

None.gif          private   object  InvokeMacro(  object  obj,  string  expression )
ExpandedBlockStart.gifContractedBlock.gif        
dot.gif {
InBlock.gif                
//子字符串的开始位置
InBlock.gif
                int subexStartIndex = 0;
InBlock.gif                
//子字符串的结束位置
InBlock.gif
                int subexEndIndex = 0;
InBlock.gif                
object subexObject = obj;
InBlock.gif                
InBlock.gif            
try
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
do
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif                    subexEndIndex 
= expression.IndexOfAny(new char[]dot.gif{'.','(','['}, subexStartIndex);
InBlock.gif                    
InBlock.gif                    
if ( subexEndIndex == -1 ) subexEndIndex=expression.Length;
InBlock.gif                    
//取得当次循环处理的宏的字符串表达式
InBlock.gif
                    string subex = expression.Substring(subexStartIndex,subexEndIndex-subexStartIndex);
InBlock.gif                    
//为下次循环做准备
InBlock.gif
                    subexStartIndex = subexEndIndex+1;
InBlock.gif                    MemberInfo memberToInvoke;
InBlock.gif                    
//搜索与子字符串匹配的对象或方法
InBlock.gif
                    MemberInfo[] members = subexObject.GetType().FindMembers(
InBlock.gif                        MemberTypes.Field
|MemberTypes.Method|MemberTypes.Property,
InBlock.gif                        BindingFlags.IgnoreCase
|BindingFlags.Instance|BindingFlags.Public,
InBlock.gif                        
new MemberFilter(this.IsMemberEligibleForMacroCall), subex.Trim() );
InBlock.gif                    
string[] arglist=null;
InBlock.gif
InBlock.gif                    
if ( members.Length == 0 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        
throw new MissingMemberException(subexObject.GetType().FullName,subex.Trim());
ExpandedSubBlockEnd.gif                    }

InBlock.gif
InBlock.gif                    
//当当前宏带有参数时,取得当前宏的参数
InBlock.gif
                    if ( subexEndIndex<expression.Length && (expression[subexEndIndex] == '[' || expression[subexEndIndex] == '(') )
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        arglist 
= SplitArgs(expression,subexEndIndex, ref subexStartIndex);
ExpandedSubBlockEnd.gif                    }

InBlock.gif
InBlock.gif                    
//SDH: We REALLY need to refactor this whole Clemens thing - it's getting hairy.
InBlock.gif
                    memberToInvoke = null;
InBlock.gif                    
if ( members.Length > 1 )
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        
foreach(MemberInfo potentialMember in members)
ExpandedSubBlockStart.gifContractedSubBlock.gif                        
dot.gif{
InBlock.gif                            MethodInfo potentialMethod 
= potentialMember as MethodInfo;
InBlock.gif                            
if(potentialMethod != null)
ExpandedSubBlockStart.gifContractedSubBlock.gif                            
dot.gif{
InBlock.gif                                ParameterInfo[] parameters 
= potentialMethod.GetParameters();
InBlock.gif                                
if(parameters != null && parameters.Length > 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif                                
dot.gif{
InBlock.gif                                    
//当参数数量相同时判定为要执行的宏
InBlock.gif
                                    if(parameters.Length == arglist.Length)
ExpandedSubBlockStart.gifContractedSubBlock.gif                                    
dot.gif{
InBlock.gif                                        memberToInvoke 
= potentialMember;
InBlock.gif                                        
break;
ExpandedSubBlockEnd.gif                                    }

ExpandedSubBlockEnd.gif                                }

ExpandedSubBlockEnd.gif                            }

ExpandedSubBlockEnd.gif                        }

ExpandedSubBlockEnd.gif                    }

InBlock.gif
InBlock.gif                    
//如果不存在该方法,则使用第一个
InBlock.gif
                    if(memberToInvoke == null)//Previous behavior, use the first one.
ExpandedSubBlockStart.gifContractedSubBlock.gif
                    dot.gif{
InBlock.gif                        memberToInvoke 
= members[0];
ExpandedSubBlockEnd.gif                    }

InBlock.gif
InBlock.gif
InBlock.gif                    
//当要执行的宏是一个属性,类似这样的东西Lupin.或者LUPIN[a,b]
InBlock.gif
                    if ( memberToInvoke.MemberType == MemberTypes.Property &&
InBlock.gif                        (subexEndIndex
==expression.Length ||
InBlock.gif                        expression[subexEndIndex] 
== '.' ||
InBlock.gif                        expression[subexEndIndex] 
== '[' ))
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        PropertyInfo propInfo 
= memberToInvoke as PropertyInfo;
InBlock.gif                        
if ( subexEndIndex<expression.Length && expression[subexEndIndex] == '[' )
ExpandedSubBlockStart.gifContractedSubBlock.gif                        
dot.gif{
InBlock.gif                            System.Reflection.ParameterInfo[] paramInfo 
= propInfo.GetIndexParameters();
InBlock.gif                            
if ( arglist.Length > paramInfo.Length )
ExpandedSubBlockStart.gifContractedSubBlock.gif                            
dot.gif{
InBlock.gif                                
throw new InvalidOperationException(String.Format("Parameter list length mismatch {0}",memberToInvoke.Name));
ExpandedSubBlockEnd.gif                            }

InBlock.gif                            
object[] oarglist = new object[paramInfo.Length];
InBlock.gif                            
forint n=0;n<arglist.Length;n++)
ExpandedSubBlockStart.gifContractedSubBlock.gif                            
dot.gif{
InBlock.gif                                oarglist[n] 
= Convert.ChangeType(arglist[n],paramInfo[n].ParameterType);
ExpandedSubBlockEnd.gif                            }

InBlock.gif                            subexObject 
= propInfo.GetValue(subexObject,oarglist);
ExpandedSubBlockEnd.gif                        }

InBlock.gif                        
else
ExpandedSubBlockStart.gifContractedSubBlock.gif                        
dot.gif{
InBlock.gif                            subexObject 
= propInfo.GetValue(subexObject,null);
ExpandedSubBlockEnd.gif                        }

ExpandedSubBlockEnd.gif                    }

InBlock.gif                    
else if ( memberToInvoke.MemberType == MemberTypes.Field &&
InBlock.gif                        (subexEndIndex
==expression.Length ||
InBlock.gif                        expression[subexEndIndex] 
== '.'))
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        FieldInfo fieldInfo 
= memberToInvoke as FieldInfo;
InBlock.gif                        subexObject 
= fieldInfo.GetValue(subexObject);
ExpandedSubBlockEnd.gif                    }

InBlock.gif                    
else if ( memberToInvoke.MemberType == MemberTypes.Method &&
InBlock.gif                                subexEndIndex
<expression.Length && expression[subexEndIndex] == '(' )
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        MethodInfo methInfo 
= memberToInvoke as MethodInfo;
InBlock.gif                        System.Reflection.ParameterInfo[] paramInfo 
= methInfo.GetParameters();
InBlock.gif                        
if ( arglist.Length > paramInfo.Length && 
InBlock.gif                            
!(paramInfo.Length>0 && paramInfo[paramInfo.Length-1].ParameterType == typeof(string[])))
ExpandedSubBlockStart.gifContractedSubBlock.gif                        
dot.gif{
InBlock.gif                            
throw new InvalidOperationException(String.Format("Parameter list length mismatch {0}",memberToInvoke.Name));
ExpandedSubBlockEnd.gif                        }

InBlock.gif                        
object[] oarglist = new object[paramInfo.Length];
InBlock.gif                        
forint n=0;n<arglist.Length;n++)
ExpandedSubBlockStart.gifContractedSubBlock.gif                        
dot.gif{
InBlock.gif                            
if ( n == paramInfo.Length-1 && 
InBlock.gif                                arglist.Length
>paramInfo.Length)
ExpandedSubBlockStart.gifContractedSubBlock.gif                            
dot.gif{
InBlock.gif                                
string[] paramsArg = new string[arglist.Length-paramInfo.Length+1];
InBlock.gif                                
forint m=n;m<arglist.Length;m++)
ExpandedSubBlockStart.gifContractedSubBlock.gif                                
dot.gif{
InBlock.gif                                    paramsArg[m
-n] = Convert.ChangeType(arglist[n],typeof(string)) as string;
ExpandedSubBlockEnd.gif                                }

InBlock.gif                                oarglist[n] 
= paramsArg;
InBlock.gif                                
break;
ExpandedSubBlockEnd.gif                            }

InBlock.gif                            
else
ExpandedSubBlockStart.gifContractedSubBlock.gif                            
dot.gif{
InBlock.gif                                oarglist[n] 
= Convert.ChangeType(arglist[n],paramInfo[n].ParameterType);
ExpandedSubBlockEnd.gif                            }

ExpandedSubBlockEnd.gif                        }

InBlock.gif                        subexObject 
= methInfo.Invoke(subexObject,oarglist);
ExpandedSubBlockEnd.gif                    }

InBlock.gif                    
ExpandedSubBlockEnd.gif                }

InBlock.gif                
while(subexEndIndex<expression.Length && subexStartIndex < expression.Length);
InBlock.gif
InBlock.gif                
return subexObject==null?new LiteralControl(""):subexObject;
ExpandedSubBlockEnd.gif            }

InBlock.gif            
catch( Exception exc )
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                ErrorTrace.Trace(System.Diagnostics.TraceLevel.Error,exc);
InBlock.gif                
throw;
ExpandedSubBlockEnd.gif            }

InBlock.gif            
//return new LiteralControl("");
ExpandedBlockEnd.gif
        }

这个函数相当于是个超简单的脚本解释引擎,具体的工作流程已经超出来本篇的讨论内容,而且再说下去这篇文章就太长了。。所以,就此打住。dasBlog的模板引擎的实现也解释得差不多了。

cptrk.ashx?id=3e9426bc-bf74-457d-ad99-bfa33e1f5be3

转载于:https://www.cnblogs.com/hillywolf/archive/2006/03/30/363085.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值