(转)基于T4模板引擎生成静态网站(CMS)

原文地址:http://www.cnblogs.com/smartbooks/archive/2012/05/13/2498258.html

缘由

用Net技术生成纯静态网站目前市面上的技术貌似不是很多,要么就是一些大公司的项目。相比于Php语言来说,基于Php语言的CMS系统就有很多了,并且模板解析技术也已经比较成熟了。模板解析引擎一直是一个核心的问题,曾经我也尝试了好多种办法来间接的实现模板解析,但都不能完美的解决面临的问题,相信很多使用Net做网站的朋友也希望有一套像Php那样的CMS系统。直到有一天公司组织微软的专家过来培训让我了解到了VS10在代码生成方面所呈现出的优越表现,让我联想到了这套引擎能不能用于其他的方面应用。。。。(写此文的目的为记录日志,所以大牛的话可以飘过了。)

一、所需准备:

  1. 本文介绍的实现方法将以C#语言为实现。
  2. 实验环境是VS10+sp1+VSsdk
  3. 需要引入程序集:Microsoft.VisualStudio.TextTemplating.10.0.dll  和 Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll
  4. 所需的Net framework平台是 4,还在用2  || 3  ||  3.5的朋友赶紧的更新一下吧!

下载地址(vssdk):http://www.microsoft.com/en-us/download/details.aspx?id=2680

已知问题:如果你的VS已经打过了SP1,那么安装VSSDK时会出现一个错误,需要手工更改一下注册表,需要将注册表中的某个键值1更改为0。具体的详细设置办法Google一下就有答案了。

二、技术实现

2.1实现思路

用T4做为模板文件的解析引擎,将数据、解析引擎、静态文件模板、控制器分别单独出来,这样的话程序员只用写一套框架程序就行了,框架写好之后剩下的就是写静态文件模板了。关于T4模板解析引擎功能的强大之处,可以参考MSDN的官方资料。()像这些问题如:模板嵌套子模板、可编程等问题,早已被T4完美的解决了。

2.2实现代码

要实现我们的自定义主机解析引擎,首先要添加一个实现了ITextTemplatingEngineHost 和  ITextTemplatingSessionHost  接口的类,代码如下(时间长了,我也忘记是从哪里Copy过来的代码了,应该是MSDN吧):

首先创建一个CustomCmdLineHost类,添加如下应用:

   1: using System.IO;
   2: using System.CodeDom.Compiler;
   3: using Microsoft.VisualStudio.TextTemplating;

然后实现ITextTemplatingEngineHost 和  ITextTemplatingSessionHost  接口,代码如下:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.IO;
   6: using System.CodeDom.Compiler;
   7: using Microsoft.VisualStudio.TextTemplating;
   8:  
   9: namespace Smart.TextTemplating
  10: {
  11:     public class CustomCmdLineHost : ITextTemplatingEngineHost, ITextTemplatingSessionHost
  12:     {
  13:         //the path and file name of the text template that is being processed
  14:         //---------------------------------------------------------------------
  15:         public string TemplateFileValue;
  16:         public string TemplateFile
  17:         {
  18:             get { return TemplateFileValue; }
  19:         }
  20:         //This will be the extension of the generated text output file.
  21:         //The host can provide a default by setting the value of the field here.
  22:         //The engine can change this value based on the optional output directive
  23:         //if the user specifies it in the text template.
  24:         //---------------------------------------------------------------------
  25:         private string fileExtensionValue = ".txt";
  26:         public string FileExtension
  27:         {
  28:             get { return fileExtensionValue; }
  29:         }
  30:         //This will be the encoding of the generated text output file.
  31:         //The host can provide a default by setting the value of the field here.
  32:         //The engine can change this value based on the optional output directive
  33:         //if the user specifies it in the text template.
  34:         //---------------------------------------------------------------------
  35:         private Encoding fileEncodingValue = Encoding.UTF8;
  36:         public Encoding FileEncoding
  37:         {
  38:             get { return fileEncodingValue; }
  39:         }
  40:         //These are the errors that occur when the engine processes a template.
  41:         //The engine passes the errors to the host when it is done processing,
  42:         //and the host can decide how to display them. For example, the host 
  43:         //can display the errors in the UI or write them to a file.
  44:         //---------------------------------------------------------------------
  45:         private CompilerErrorCollection errorsValue;
  46:         public CompilerErrorCollection Errors
  47:         {
  48:             get { return errorsValue; }
  49:         }
  50:         //The host can provide standard assembly references.
  51:         //The engine will use these references when compiling and
  52:         //executing the generated transformation class.
  53:         //--------------------------------------------------------------
  54:         public IList<string> StandardAssemblyReferences
  55:         {
  56:             get
  57:             {
  58:                 return new string[]
  59:                 {
  60:                     //If this host searches standard paths and the GAC,
  61:                     //we can specify the assembly name like this.
  62:                     //---------------------------------------------------------
  63:                     //"System"
  64:  
  65:                     //Because this host only resolves assemblies from the 
  66:                     //fully qualified path and name of the assembly,
  67:                     //this is a quick way to get the code to give us the
  68:                     //fully qualified path and name of the System assembly.
  69:                     //---------------------------------------------------------
  70:                     typeof(System.Uri).Assembly.Location
  71:                 };
  72:             }
  73:         }
  74:         //The host can provide standard imports or using statements.
  75:         //The engine will add these statements to the generated 
  76:         //transformation class.
  77:         //--------------------------------------------------------------
  78:         public IList<string> StandardImports
  79:         {
  80:             get
  81:             {
  82:                 return new string[]
  83:                 {
  84:                     "System"
  85:                 };
  86:             }
  87:         }
  88:         //The engine calls this method based on the optional include directive
  89:         //if the user has specified it in the text template.
  90:         //This method can be called 0, 1, or more times.
  91:         //---------------------------------------------------------------------
  92:         //The included text is returned in the context parameter.
  93:         //If the host searches the registry for the location of include files,
  94:         //or if the host searches multiple locations by default, the host can
  95:         //return the final path of the include file in the location parameter.
  96:         //---------------------------------------------------------------------
  97:         public bool LoadIncludeText(string requestFileName, out string content, out string location)
  98:         {
  99:             content = System.String.Empty;
 100:             location = System.String.Empty;
 101:  
 102:             //If the argument is the fully qualified path of an existing file,
 103:             //then we are done.
 104:             //----------------------------------------------------------------
 105:             if (File.Exists(requestFileName))
 106:             {
 107:                 content = File.ReadAllText(requestFileName);
 108:                 return true;
 109:             }
 110:             //This can be customized to search specific paths for the file.
 111:             //This can be customized to accept paths to search as command line
 112:             //arguments.
 113:             //----------------------------------------------------------------
 114:             else
 115:             {
 116:                 return false;
 117:             }
 118:         }
 119:         //Called by the Engine to enquire about 
 120:         //the processing options you require. 
 121:         //If you recognize that option, return an 
 122:         //appropriate value. 
 123:         //Otherwise, pass back NULL.
 124:         //--------------------------------------------------------------------
 125:         public object GetHostOption(string optionName)
 126:         {
 127:             object returnObject;
 128:             switch (optionName)
 129:             {
 130:                 case "CacheAssemblies":
 131:                     returnObject = true;
 132:                     break;
 133:                 default:
 134:                     returnObject = null;
 135:                     break;
 136:             }
 137:             return returnObject;
 138:         }
 139:         //The engine calls this method to resolve assembly references used in
 140:         //the generated transformation class project and for the optional 
 141:         //assembly directive if the user has specified it in the text template.
 142:         //This method can be called 0, 1, or more times.
 143:         //---------------------------------------------------------------------
 144:         public string ResolveAssemblyReference(string assemblyReference)
 145:         {
 146:             //If the argument is the fully qualified path of an existing file,
 147:             //then we are done. (This does not do any work.)
 148:             //----------------------------------------------------------------
 149:             if (File.Exists(assemblyReference))
 150:             {
 151:                 return assemblyReference;
 152:             }
 153:             //Maybe the assembly is in the same folder as the text template that 
 154:             //called the directive.
 155:             //----------------------------------------------------------------
 156:             string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
 157:             if (File.Exists(candidate))
 158:             {
 159:                 return candidate;
 160:             }
 161:             //This can be customized to search specific paths for the file
 162:             //or to search the GAC.
 163:             //----------------------------------------------------------------
 164:             //This can be customized to accept paths to search as command line
 165:             //arguments.
 166:             //----------------------------------------------------------------
 167:             //If we cannot do better, return the original file name.
 168:             return "";
 169:         }
 170:         //The engine calls this method based on the directives the user has 
 171:         //specified in the text template.
 172:         //This method can be called 0, 1, or more times.
 173:         //---------------------------------------------------------------------
 174:         public Type ResolveDirectiveProcessor(string processorName)
 175:         {
 176:             //This host will not resolve any specific processors.
 177:             //Check the processor name, and if it is the name of a processor the 
 178:             //host wants to support, return the type of the processor.
 179:             //---------------------------------------------------------------------
 180:             if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) == 0)
 181:             {
 182:                 //return typeof();
 183:             }
 184:             //This can be customized to search specific paths for the file
 185:             //or to search the GAC
 186:             //If the directive processor cannot be found, throw an error.
 187:             throw new Exception("Directive Processor not found");
 188:         }
 189:         //A directive processor can call this method if a file name does not 
 190:         //have a path.
 191:         //The host can attempt to provide path information by searching 
 192:         //specific paths for the file and returning the file and path if found.
 193:         //This method can be called 0, 1, or more times.
 194:         //---------------------------------------------------------------------
 195:         public string ResolvePath(string fileName)
 196:         {
 197:             if (fileName == null)
 198:             {
 199:                 throw new ArgumentNullException("the file name cannot be null");
 200:             }
 201:             //If the argument is the fully qualified path of an existing file,
 202:             //then we are done
 203:             //----------------------------------------------------------------
 204:             if (File.Exists(fileName))
 205:             {
 206:                 return fileName;
 207:             }
 208:             //Maybe the file is in the same folder as the text template that 
 209:             //called the directive.
 210:             //----------------------------------------------------------------
 211:             string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
 212:             if (File.Exists(candidate))
 213:             {
 214:                 return candidate;
 215:             }
 216:             //Look more places.
 217:             //----------------------------------------------------------------
 218:             //More code can go here...
 219:             //If we cannot do better, return the original file name.
 220:             return fileName;
 221:         }
 222:         //If a call to a directive in a text template does not provide a value
 223:         //for a required parameter, the directive processor can try to get it
 224:         //from the host by calling this method.
 225:         //This method can be called 0, 1, or more times.
 226:         //---------------------------------------------------------------------
 227:         public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
 228:         {
 229:             if (directiveId == null)
 230:             {
 231:                 throw new ArgumentNullException("the directiveId cannot be null");
 232:             }
 233:             if (processorName == null)
 234:             {
 235:                 throw new ArgumentNullException("the processorName cannot be null");
 236:             }
 237:             if (parameterName == null)
 238:             {
 239:                 throw new ArgumentNullException("the parameterName cannot be null");
 240:             }
 241:             //Code to provide "hard-coded" parameter values goes here.
 242:             //This code depends on the directive processors this host will interact with.
 243:             //If we cannot do better, return the empty string.
 244:             return String.Empty;
 245:         }
 246:         //The engine calls this method to change the extension of the 
 247:         //generated text output file based on the optional output directive 
 248:         //if the user specifies it in the text template.
 249:         //---------------------------------------------------------------------
 250:         public void SetFileExtension(string extension)
 251:         {
 252:             //The parameter extension has a '.' in front of it already.
 253:             //--------------------------------------------------------
 254:             fileExtensionValue = extension;
 255:         }
 256:         //The engine calls this method to change the encoding of the 
 257:         //generated text output file based on the optional output directive 
 258:         //if the user specifies it in the text template.
 259:         //----------------------------------------------------------------------
 260:         public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
 261:         {
 262:             fileEncodingValue = encoding;
 263:         }
 264:         //The engine calls this method when it is done processing a text
 265:         //template to pass any errors that occurred to the host.
 266:         //The host can decide how to display them.
 267:         //---------------------------------------------------------------------
 268:         public void LogErrors(CompilerErrorCollection errors)
 269:         {
 270:             errorsValue = errors;
 271:         }
 272:         //This is the application domain that is used to compile and run
 273:         //the generated transformation class to create the generated text output.
 274:         //----------------------------------------------------------------------
 275:         public AppDomain ProvideTemplatingAppDomain(string content)
 276:         {
 277:             //This host will provide a new application domain each time the 
 278:             //engine processes a text template.
 279:             //-------------------------------------------------------------
 280:             return AppDomain.CreateDomain("Generation App Domain");
 281:             //This could be changed to return the current appdomain, but new 
 282:             //assemblies are loaded into this AppDomain on a regular basis.
 283:             //If the AppDomain lasts too long, it will grow indefintely, 
 284:             //which might be regarded as a leak.
 285:             //This could be customized to cache the application domain for 
 286:             //a certain number of text template generations (for example, 10).
 287:             //This could be customized based on the contents of the text 
 288:             //template, which are provided as a parameter for that purpose.
 289:         }
 290:  
 291:         public ITextTemplatingSession CreateSession()
 292:         {
 293:             return Session;
 294:         }
 295:  
 296:         public ITextTemplatingSession Session
 297:         {
 298:             get;
 299:             set;
 300:         }
 301:     }
 302: }

ITextTemplatingEngineHost  毫无疑问是模板解析引擎的主机;实现ITextTemplatingSessionHost  接口可以让我们往模板中传递变量,它采用了asp.net 中的Session概念。如果我们不需要往模板中传递Session数据话,可以不实现这个接口。

调用实例一:

   1: Smart.TextTemplating.CustomCmdLineHost host = new Smart.TextTemplating.CustomCmdLineHost();
   2:             Engine engine = new Engine();
   3:             host.Session = new TextTemplatingSession();
   4:             host.Session["count"] = 5;
   5:             host.TemplateFileValue = "tmp.tt";
   6:             string input = File.ReadAllText(host.TemplateFileValue);
   7:             string output = engine.ProcessTemplate(input, host);
   8:             File.WriteAllText(host.TemplateFileValue + ".txt", output);

模板文件内容(tmp.tt):

   1: <#@ template debug="true" #>
   2: <#@ parameter name="data" type="System.Object" #>
   3:  
   4: <# 
   5:    int count = Convert.ToInt32(data);
   6:    for (int i=0; i<count; i++)
   7:    {
   8:        WriteLine(i.ToString());
   9:    }   
  10: #>

调用实例二:

此种方法我封装了一个单独的类,加入了我的代码收藏夹,实现了多文件生成,向模板文件传递数据等:

调用示例:

   1: List<object> data = new List<object>();
   2:             for (int i = 0; i < 5; i++)
   3:             {
   4:                 data.Add(i.ToString());
   5:             }
   6:  
   7:             Smart.TextTemplating.ParseTextTemplating parse = new Smart.TextTemplating.ParseTextTemplating(
   8:                 "",
   9:                 "view_",
  10:                 ".txt",
  11:                 data,
  12:                 "tmp.tt");
  13:             parse.Parse();

我封装的代码(以后想用了直接调用就可以了):

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.CodeDom.Compiler;
   6: using System.IO;
   7: using Microsoft.VisualStudio.TextTemplating;
   8:  
   9: namespace Smart.TextTemplating
  10: {
  11:     public class ParseTextTemplating
  12:     {
  13:         #region 私有变量
  14:         private string _templateContent;
  15:         private string _saveRootPath;
  16:         private string _startFlag;
  17:         private string _extension;
  18:         private List<object> _data = new List<object>();
  19:         #endregion
  20:  
  21:         public ParseTextTemplating(string saveRootPath, string startFlag, string extension, List<object> data, string templatefilePath)
  22:         {
  23:             this._saveRootPath = saveRootPath;
  24:             this._startFlag = startFlag;
  25:             this._extension = extension;
  26:             this._data = data;
  27:  
  28:             if (string.IsNullOrEmpty(_saveRootPath))
  29:             {
  30:                 _saveRootPath = AppDomain.CurrentDomain.BaseDirectory;
  31:             }
  32:             if (string.IsNullOrEmpty(_extension))
  33:             {
  34:                 _extension = ".html";
  35:             }
  36:             if (string.IsNullOrEmpty(templatefilePath) || !File.Exists(templatefilePath))
  37:             {
  38:                 this._templateContent = "";
  39:             }
  40:             else
  41:             {
  42:                 this._templateContent = File.ReadAllText(templatefilePath);
  43:             }
  44:         }
  45:  
  46:         public void Parse()
  47:         {
  48:             foreach (object key in _data)
  49:             {
  50:                 SmartTextTemplatingEngineHost host = new SmartTextTemplatingEngineHost();
  51:                 host.Session = new TextTemplatingSession();
  52:                 host.Session["data"] = key;
  53:                 Engine engine = new Engine();
  54:                 string content = engine.ProcessTemplate(_templateContent, host);
  55:                 if (!string.IsNullOrEmpty(content) && host.Errors.Count == 0)
  56:                 {
  57:                     string filePath = string.Format("{0}{1}{2}{3}",
  58:                         _saveRootPath,
  59:                         _startFlag,
  60:                         DateTime.Now.ToString("yyyyMMddHHmmss") + DateTime.Now.Millisecond.ToString("d4"),
  61:                         _extension);
  62:                     File.WriteAllText(filePath, content, Encoding.UTF8);
  63:                 }
  64:  
  65:                 foreach (CompilerError er in host.Errors)
  66:                 {
  67:                     File.AppendAllText(_saveRootPath + "error.txt",
  68:                         er.ToString() + "\r\n\r\n",
  69:                         Encoding.UTF8);
  70:                 }
  71:             }
  72:         }
  73:     }
  74: }

模板文件传递变量:<#@ parameter name="data" type="System.Object" #> 可以接口主程序传递过来的onject类型的变量数据。

2.3运行结果

生成的结果文件:

image_thumb

文件内容:

image_thumb1

三、总结和展望

T4模板引擎是非常强大的,至于功能都强大到何处还需要我们深入的仔细了解。细心的同学可能已经发现了,T4模板引擎虽然强大,但是写模板时却没有一个像VS那样的带智能感知的代码提示工具啊?放心吧!这个问题已经不是问题了,安装一个VS扩展就行了,看截图:

image_thumb2

下载地址我就不贴了,文件名字是“tangibleT4EditorPlusModellingToolsSetup.msi”,相信有了Google和文件的名字找到官方网站的主页和下载地址对你已经不是问题了,如果从VS扩展管理器安装的话,名字是(推荐了2个):t4 editor 和visual t4。

什么?T4模板的语法太古怪!写起来太麻烦!!写起来太累!程序员对代码总是这么苛刻,好吧,满足你的苛刻要求,不过要下次才能向你介绍了:

下次向你介绍的基于Net的模板解析引擎名字是:Razor

调用示例:

image_thumb4

http://www.codeplex.com/上有个项目,名字是:RazorEngine 地址:http://razorengine.codeplex.com/

Razor的语法预览:

image_thumb3

Razor语法详细介绍:http://weblogs.asp.net/scottgu/archive/2010/07/02/introducing-razor.aspx

原文地址:https://www.cnblogs.com/fcsh820/p/2638372.html