最近遇到一個項目,要使用RazorEngine做模板引擎,然后完成簡易的CMS功能,以減輕重復的CDRU操作,同時復用管理后台。沒錯,使用的正是GIT HUB上的開源項目:https://github.com/Antaris/RazorEngine 。模板編譯過程非常耗時,所以Razor提供了Compile和Parse的帶key參數的重載,以實現從緩存中加載編譯后的模板的功能。不過這里還是有一個問題,對於web項目而言,應用程序池會周期性的回收(即使設置了不自動回收,不知為何)。所以,仍然會存在重新編譯而導致頁面長時間掛起的問題。或許可以提供一個進度條,告訴客戶這是一個高大上的東西,需要熱身....不過應該有更好的辦法,就是從RazorEngine本身着手。
RazorEngine接收到模板內容的時候,會調用編輯器將其編譯成一個程序集,加載到內存中,同時返回編譯好的對應的模板的類型對象(Type對象)。然后調用對象的構造函數,生成對象實例,最后“執行”模板。BUT!為什么是加載到內存呢,如果是將程序集保存在磁盤上,那么下次再進行讀取的話,這個性能絕對不是同一個檔次的。所以,考慮之后,決定用以下的思路解決問題:
獲取到模板之后(字符串),會計算其MD5值,並作為生成的模板類型的Class(有坑),同時將其作為程序集的名稱。每當客戶機代碼請求編譯模板的時候,就去指定的目錄查找是否有這么一個程序集,如果有,直接加載這個程序集,並返回類型信息(因為程序集中只有一個類型,所以非常方便)。RazorEngine本身采用了可擴展的設計。擴展口就在Razor.SetTemplateService()。只需要實現CompilerServiceBase抽象類,就能自定義模板引擎的邏輯。只是...代碼編譯部分處於調用的較底層,如果全部采用全新的實現的話...工作量不少,而且容易存在BUG。SO...原樣應用RazorEngine.dll並進行擴展這么完美的事情暫時還辦不到。我采用的做法是,修改RazorEngine的源碼,但是不修改已經定義的類型,只在相應的名稱空間下面提供新的類型來滿足自己的需求(因為其中不少需要的類型是internal的)。
參考TemplateService的源碼,實現了一個ReloadableTemplateService,重新實現了CreateTemplateType方法:
[Pure] public virtual Type CreateTemplateType(string razorTemplate, Type modelType) { //重要:類名不能以數字開頭 var key = GetTemplateMd5(razorTemplate); string className = "C" + key; var assemblyPath = AssemblyDirecotry.TrimEnd(new[] { '/', '\\' }) + "\\" + key + ".dll"; if (File.Exists(assemblyPath)) { try { //var assembly = Assembly.Load(File.ReadAllBytes(assemblyPath)); var assembly = Assembly.LoadFile(assemblyPath); return assembly.GetTypes()[0]; } catch (Exception ex) { Debug.WriteLine("an error ocured and assembly has been deleted"); File.Delete(assemblyPath); } } var context = new TypeContext { ModelType = (modelType == null) ? typeof(object) : modelType, TemplateContent = razorTemplate, TemplateType = (_config.BaseTemplateType) ?? typeof(TemplateBase<>), }; foreach (string ns in _config.Namespaces) context.Namespaces.Add(ns); //csharp only var service = new CSharpFileDirectCompilerService(); service.Debug = _config.Debug; service.CodeInspectors = _config.CodeInspectors ?? Enumerable.Empty<ICodeInspector>(); var result = service.CompileType(context, className, assemblyPath); _assemblies.Add(result.Item2); return result.Item1; }
需要注意是,C#的類型名稱不能以數字開頭...這個問題我查了一下午,主要是壓根沒有想到這一點。生成程序集的方法,被放置在CSharpFileDirectCompilerService中,由於這個類型的局限性很強(我只是想快速解決問題),所以沒有實現基類要求的方法(我把它廢了,雖然這樣很傻逼):
[Pure] private Tuple<CompilerResults, string> Compile(TypeContext context, string className, string assemblyPath) { if (_disposed) throw new ObjectDisposedException(GetType().Name); var compileUnit = GetCodeCompileUnit(className, context.TemplateContent, context.Namespaces, context.TemplateType, context.ModelType); var @params = new CompilerParameters { GenerateInMemory = false, OutputAssembly = assemblyPath, GenerateExecutable = false, IncludeDebugInformation = false, CompilerOptions = "/target:library /optimize /define:RAZORENGINE" }; var assemblies = CompilerServicesUtility .GetLoadedAssemblies() .Where(a => !a.IsDynamic && File.Exists(a.Location)) .GroupBy(a => a.GetName().Name) .Select(grp => grp.First(y => y.GetName().Version == grp.Max(x => x.GetName().Version))) // only select distinct assemblies based on FullName to avoid loading duplicate assemblies .Select(a => a.Location); var includeAssemblies = (IncludeAssemblies() ?? Enumerable.Empty<string>()); assemblies = assemblies.Concat(includeAssemblies) .Where(a => !string.IsNullOrWhiteSpace(a)) .Distinct(StringComparer.InvariantCultureIgnoreCase); @params.ReferencedAssemblies.AddRange(assemblies.ToArray()); string sourceCode = null; if (Debug) { var builder = new StringBuilder(); using (var writer = new StringWriter(builder, CultureInfo.InvariantCulture)) { _codeDomProvider.GenerateCodeFromCompileUnit(compileUnit, writer, new CodeGeneratorOptions()); sourceCode = builder.ToString(); } } return Tuple.Create(_codeDomProvider.CompileAssemblyFromDom(@params, compileUnit), sourceCode); } /// <summary> /// Compiles the type defined in the specified type context. /// </summary> /// <param name="context">The type context which defines the type to compile.</param> /// <returns>The compiled type.</returns> [Pure, SecurityCritical,Obsolete("該方法無法兼容其父類,功能已經在其重載中提供")] public override Tuple<Type, Assembly> CompileType(TypeContext context) { throw new NotImplementedException("該方法無法兼容其父類,功能已經在其重載中提供"); } /// <summary> /// /// </summary> /// <param name="context"></param> /// <param name="className"></param> /// <param name="assemblyDirectory"></param> /// <returns></returns> /// <exception cref="NullReferenceException"></exception> /// <exception cref="TemplateCompilationException"></exception> public Tuple<Type, Assembly> CompileType(TypeContext context, string className, string assemblyDirectory) { if (context == null) throw new NullReferenceException("context"); var result = Compile(context, className, assemblyDirectory); var compileResult = result.Item1; if (compileResult.Errors != null && compileResult.Errors.HasErrors) throw new TemplateCompilationException(compileResult.Errors, result.Item2, context.TemplateContent); return Tuple.Create( compileResult.CompiledAssembly.GetType("CompiledRazorTemplates.Dynamic." + className), compileResult.CompiledAssembly); }
然后就成了,調用方式是:
Razor.SetTemplateService(new ReloadableTemplateService() { AssemblyDirecotry = "d:\\temple" });
稍微測了下性能,這里定性描述下:編譯模板的時候,耗時會根據模板的大小和復雜度,一兩秒或者更多。而加載一個程序集的話,尤其是像這種一個類型的小程序集,總是毫秒級的。
真是個愉快的周末。