Razor模板引擎
一、簡介
在MVC以外的場景中,我們往往需要完成一些模板引擎生成代碼或頁面的工作;在以前我們一般常用的有Razor、NVeocity、VTemplate。雖然所有的模板系統都具有一些共同特征,但 Razor卻和我們前面討論的二種視圖引擎截然不同。不同於其它視圖引擎,Razor在使用XML代 碼方面沒有走得那么極端。它也不完全類似於ASPX,因為它把那些比較笨重的占位符替換成@符號接表達式或者普通的控制塊。因為不需要特殊的結束標記,所 以Razor最終的代碼很簡練。
本篇介紹的主角是Razor,在非Core的版本中,我們常用開源的RazorEngine來解決我們的問題;但是它卻沒有對應.NET Core的版本。我們也只要自己動手來完成一個支持.NET Core的“模板引擎”版本。
一般情況下使用Razor作為視圖引擎要實現如下步驟:
(1)讀取模板文件 -> (2)生成Raozr的C#代碼 -> (3)使用Roslyn編譯代碼生成程序集 -> (4)動態加載程序集 -> (5)反射調用
二、非Mvc中使用Razor
我們一般在使用Razor時都是在ASP.NET MVC中使用.cshtml來作為模板,由ASP.NET MVC的視圖引擎(ViewEngine)來生成頁面的代碼的,總之,這里想說的是,模板引擎是獨立的,它們甚至是獨立的項目,由不同的公司和組織來開發。這次我們要在非Mvc中使用Raozr;首先我們要“脫離”Mvc的環境。
這里我們只在.NET Core程序中引用微軟Raozr部分的程序集Microsoft.AspNetCore.Razor 1.0版本,這個程序集負責將模板生成出C#代碼。
1.Project.json添加引用
- "dependencies": {
- "Microsoft.AspNetCore.Razor": "1.0.0"
- "NETStandard.Library": "1.6.0"
- }
2.模板生成代碼
如下是摘錄的YOYOFx框架中的一段代碼,因為我們要生成代碼時一般需要傳入Model數據,這時需要Model Type組織代碼時,要將泛型的情況考慮進去 。這里的RazorViewTemplate是一個模板基類,這里包含了模板中調用的外部方法,我們常用到的如HtmlHelper、Render、Url、Raw等方法或類都是通過這個櫃頂模板定義的,RazorViewTemplate是一個自定義類不需要繼承其它類型,如果想擴展模板中使用的方法,只需要在這個類中加入即可。
- public class CodeGenerateService
- {
- public GeneratorResults Generate(Type modelType,string template)
- {
- //准備臨時類名,讀取模板文件和Razor代碼生成器
- string templateType = "YOYO.AspNetCore.ViewEngine.Razor.RazorViewTemplate";
- string templateTypeName = modelType != null ? string.Format(templateType + @"<{0}>", modelType.FullName) : templateType;
- var class_name = "c" + Guid.NewGuid().ToString("N");
- var host = new RazorEngineHost(new CSharpRazorCodeLanguage(), () => new HtmlMarkupParser())
- {
- DefaultBaseClass = templateTypeName,
- DefaultClassName = class_name,
- DefaultNamespace = "YOYO.AspNetCore.ViewEngine.Razor",
- GeneratedClassContext =
- new GeneratedClassContext("Execute", "Write", "WriteLiteral", "WriteTo",
- "WriteLiteralTo",
- "RazorViewTemplate.Dynamic", new GeneratedTagHelperContext())
- };
- host.NamespaceImports.Add("System");
- host.NamespaceImports.Add("System.Dynamic");
- host.NamespaceImports.Add("System.Linq");
- host.NamespaceImports.Add("System.Collections.Generic");
- var engine = new RazorTemplateEngine(host);
- return engine.GenerateCode(new StringReader(template));
- }
- }
通過以上代碼得到GeneratorResults類型的結果,從而可以得知生成過程是否成功,錯誤在位置等信息。最后通過GeneratedCode屬性,得到生成好的C#代碼。
3.編譯模板
一般Razor的C#代碼生成后,都是通過CodeDom來生成和編譯代碼的;.NET開源后,我們又多了一個強大的選擇Roslyn , Roslyn也是支持.NET Core的,並且在整個.NET平台中,扮演着非常重要的角色,小到這種視圖代碼編譯,大到整個項目的編譯場景都有Roslyn的身影。微軟最新開源的Visual Studio Code中C#插件,OmniSharp就是通過Roslyn來對項目和編輯器提供支持的。
摘錄YOYOFx代碼如下:
- public Type Compile(string compilationContent)
- {
- var assemblyName = Path.GetRandomFileName();
- var sourceText = SourceText.From(compilationContent, Encoding.UTF8);
- var syntaxTree = CSharpSyntaxTree.ParseText( sourceText, path: assemblyName, options: new CSharpParseOptions());
- var compilation = CSharpCompilation.Create(assemblyName, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
- syntaxTrees: new[] { syntaxTree },
- references: ApplicationReferences );
- using (var assemblyStream = new MemoryStream())
{ - var result = compilation.Emit(assemblyStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
- this.CompileResult = new CompileResult(){ Success = result.Success, Errors = result.Diagnostics.Select(d => d.ToString()).ToList()};
- if (!result.Success){
- if (!compilation.References.Any() && !ApplicationReferences.Any())
- throw new InvalidOperationException("project.json preserveCompilationContext");
- return null;
- }
- var templateType = LoadTypeForAssemblyStream(assemblyStream, null);
- return templateType;
- }
- }
在代碼中可以通過CompileResult得到相應的編譯錯誤信息,同樣包括錯誤的信息和具體錯誤所在的行。
其中注意的是LoadTypeForAssemblyStream方法,因為在.NET Core中動態加載程序集的方式跟以前有所不同AppDomain的概念現在已經消失,所以要在.NET Core動態加載程序集要使用,命名空間System.Runtime.Loader中的AssemblyLoadContext.Default.LoadFromStream 方法,而在.NET 4.5+中要使用Assembly.Load方法。
三、總結
Razor 不僅僅使用了動態的編譯,還有一個強大的模板解析的功能。利用自定義的模板基類還可以在模板里提供一些輔助方法。這樣看來 Razor 也算是 C# DSL 的一種實現了。
GitHub:https://github.com/maxzhang1985/YOYOFx 如果覺還可以請Star下, 歡迎一起交流。