原文来自 Creating a New View Engine in ASP.NET Core
原作者保留一切权利
疯狂的想法 几个月前,Taylor Mullen 在”The Monsters Weekly” 中谈到了关于 ASP.NET Core 里的 Razor 引擎。在采访中的某个时刻有人指出,在 MVC 的设计中,我们可以轻易地向框架内插入一个新的模板引擎。还有人指出,实现一个模板引擎实际上是一件非常复杂的事情。这令我们去思考,如果我们有一个现有的模板引擎的话应该怎么办?向 MVC 框架中插入一个新的模板引擎到底有多简单?
寻找一个代替品 我们想选择和 Razor 不太一样的东西,就像是 Simon 推荐的在 Express 框架中非常流行的 Pug 。而在语法方面,Pug 和 Razor 之间略有不同,Pug 使用空格来表示嵌套的元素并略去了尖括号,就像下面这样:
1 2 div a(href='google.com') Google
这一段将会生成这种 HTML:
1 2 3 <div > <a href ="google.com" > Google</a > </div >
在 ASP.NET Core 中使用 Pug 我们所面临的第一个问题是如何在 ASP.NET Core 程序中编译 Pug 模板,而 Pug 是一个基于 JavaScript 的模板引擎,但是我们只有一天的时间,所以把 Pug 移植到 C# 是不太可能的。
我们的第一个想法是使用 Edgejs 来调用 Pug 的 Compile 方法,一些快速原型向我们展示出这是可以做到的,但是 Edgejs 并不支持.NET Core,这将引导我们去使用由 ASP.NET Core 团队编写的包 “JavaScriptServices”,特别是那个可以让我们在 ASP.NET Core 程序中轻易调用 JavaScript 模块的 “Node Services” 包。
令我们惊喜的是,这个包不仅仅能够工作,而且非常易用!先创建一个叫做 pugcompile.js 的文件。
1 2 3 4 5 6 var pug = require ('pug' );module .exports = function (callback, viewPath, model ) { var pugCompiledFunction = pug.compileFile (viewPath); callback (null , pugCompiledFunction (model)); };
多亏了 Node Services,在 C# 中调用 JavaScript 是如此简单。假设 model
是我们想绑定到模板的 ViewModel,mytemplate.pug
是包含 Pug 模板的文件。
1 var html = await _nodeServices.InvokeAsync<string >("pugcompile" , "mytemplate.pug" , model);
现在我们已经证明这么做是可行的,是时候创建一个模板引擎并将其与 MVC 框架整合的时候的了。
创建 Pugzor 模板引擎 我们出于好玩决定把我们的模板引擎命名为 Pug 和 Razor 的组合:Pugzor。当然了这并不是很有意义,因为它和 Razor 并没有关系。
始终要记得,我们的目标是在一天之内实现一个模板引擎,我们希望敏捷开发。在花了点时间看了一下 MVC 的源代码之后,我们确定下来需要实现 IViewEngine
接口和自定义的 IView
。
IViewEngine
负责根据 ActionContext
和 ViewName
来确定 View
的位置。当 Controller
返回一个 View
时,它实际上是 IViewEngine
中一个用于根据约定来返回 View
的 FindView
方法。FindView
方法返回一个 ViewEngineResult
,ViewEngineResult
是一个拥有两个属性的简单的类,其中一个是 bool Success
属性,用于表明是否找到了一个 View,另一个是包含该 View(如果找到)的 IView View
属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface IViewEngine { ViewEngineResult FindView (ActionContext context, string viewName, bool isMainPage ) ; ViewEngineResult GetView (string executingFilePath, string viewPath, bool isMainPage ) ; }
我们决定使用与 Razor 相同的 View 位置约定,也就是说,View 位于 Views/{ControllerName}/{ActionName}.pug.
中。 以下是 PugzorViewEngine 的 FindView
方法的简化版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public ViewEngineResult FindView ( ActionContext actionContext, string viewName, bool isMainPage ){ var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey); var checkedLocations = new List<string >(); foreach (var location in _options.ViewLocationFormats) { var view = string .Format(location, viewName, controllerName); if (File.Exists(view)) return ViewEngineResult.Found("Default" , new PugzorView(view, _nodeServices)); checkedLocations.Add(view); } return ViewEngineResult.NotFound(viewName, checkedLocations); }
你可以在 Github 上找到完整的实现。
接下来,创建一个 PugzorView
类来实现 IView
接口,PugzorView
接受一个 pub 模板的路径和一个 INodeServices
实例。当 MVC 框架需要渲染视图时,它会调用 IView
的 RenderAsync
方法,在这个方法中,调用 pugcompile
并将生成的 HTML 写入视图上下文。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class PugzorView : IView { private string _path; private INodeServices _nodeServices; public PugzorView (string path, INodeServices nodeServices ) { _path = path; _nodeServices = nodeServices; } public string Path { get { return _path; } } public async Task RenderAsync (ViewContext context ) { var result = await _nodeServices.InvokeAsync<string >("./pugcompile" , Path, context.ViewData.Model); context.Writer.Write(result); } }
唯一剩下的就是配置 MVC 来使用我们的新模板引擎。一开始,我们认为在将 MVC 添加到服务集合中时可以很方便地使用 AddViewOptions
扩展方法来添加一个新的模板引擎。
1 2 3 4 5 6 services.AddMvc() .AddViewOptions(options => { options.ViewEngines.Add(new PugzorViewEngine(nodeServices)); });
但是这也是令我们困惑的地方,因为我们无法在 Startup.ConfigureServices
方法中为 ViewEngines
集合添加 PugzorViewEngine
的具体实例,因为模板引擎的构造函数需要依赖注入。PugzorViewEngine
依赖了 INodeServices
并且我们希望这个参数能够由 ASP.NET 的 DI 框架注入。不过幸运的是,对 Razor 无所不知的大师 Taylor Mullen 会向我们展示注册模板引擎的正确方法。
将模板引擎添加到 MVC 中的推荐方法是创建一个实现了 IConfigureOptions <MvcViewOptions>
接口的 “安装” 类,这个类通过构造器注入得到了我们的 IPugzorViewEngine
实例对象,而在 ConfigureServices
方法中,这个实例对象会被添加到 MvcViewOptions
的模板引擎列表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class PugzorMvcViewOptionsSetup : IConfigureOptions <MvcViewOptions >{ private readonly IPugzorViewEngine _pugzorViewEngine; public PugzorMvcViewOptionsSetup (IPugzorViewEngine pugzorViewEngine ) { if (pugzorViewEngine == null ) { throw new ArgumentNullException(nameof (pugzorViewEngine)); } _pugzorViewEngine = pugzorViewEngine; } public void Configure (MvcViewOptions options ) { if (options == null ) { throw new ArgumentNullException(nameof (options)); } options.ViewEngines.Add(_pugzorViewEngine); } }
现在我们需要做的是在 Startup.ConfigureServices
方法中注册 “安装” 类和模板引擎。
1 2 services.AddTransient<IConfigureOptions<MvcViewOptions>, PugzorMvcViewOptionsSetup>(); services.AddSingleton<IPugzorViewEngine, PugzorViewEngine>();
于是就像魔法一样,我们有了一个可以正常工作的模板引擎。这里有一个演示:Controllers/HomeController.cs
1 2 3 4 5 6 public IActionResult Index (){ ViewData.Add("Title" , "Welcome to Pugzor!" ); ModelState.AddModelError("model" , "An error has occurred" ); return View(new { People = A.ListOf<Person>() }); }
Views/Home/Index.pug
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 block body h2 Hello p #{ViewData.title} table(class='table') thead tr th Name th Title th Age tbody each val in people tr td= val.firstName td= val.title td= val.age
输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <h2 > Hello</h2 > <p > Welcome to Pugzor! </p > <table class ="table" > <thead > <tr > <th > Name</th > <th > Title</th > <th > Age</th > </tr > </thead > <tbody > <tr > <td > Laura</td > <td > Mrs.</td > <td > 38</td > </tr > <tr > <td > Gabriel</td > <td > Mr. </td > <td > 62</td > </tr > <tr > <td > Judi</td > <td > Princess</td > <td > 44</td > </tr > <tr > <td > Isaiah</td > <td > Air Marshall</td > <td > 39</td > </tr > <tr > <td > Amber</td > <td > Miss.</td > <td > 69</td > </tr > <tr > <td > Jeremy</td > <td > Master</td > <td > 92</td > </tr > <tr > <td > Makayla</td > <td > Dr.</td > <td > 15</td > </tr > <tr > <td > Sean</td > <td > Mr. </td > <td > 5</td > </tr > <tr > <td > Lillian</td > <td > Mr. </td > <td > 3</td > </tr > <tr > <td > Brandon</td > <td > Doctor</td > <td > 88</td > </tr > <tr > <td > Joel</td > <td > Miss.</td > <td > 12</td > </tr > <tr > <td > Madeline</td > <td > General</td > <td > 67</td > </tr > <tr > <td > Allison</td > <td > Mr. </td > <td > 21</td > </tr > <tr > <td > Brooke</td > <td > Dr.</td > <td > 27</td > </tr > <tr > <td > Jonathan</td > <td > Air Marshall</td > <td > 63</td > </tr > <tr > <td > Jack</td > <td > Mrs.</td > <td > 7</td > </tr > <tr > <td > Tristan</td > <td > Doctor</td > <td > 46</td > </tr > <tr > <td > Kandra</td > <td > Doctor</td > <td > 47</td > </tr > <tr > <td > Timothy</td > <td > Ms.</td > <td > 83</td > </tr > <tr > <td > Milissa</td > <td > Dr.</td > <td > 68</td > </tr > <tr > <td > Lekisha</td > <td > Mrs.</td > <td > 40</td > </tr > <tr > <td > Connor</td > <td > Dr.</td > <td > 73</td > </tr > <tr > <td > Danielle</td > <td > Princess</td > <td > 27</td > </tr > <tr > <td > Michelle</td > <td > Miss.</td > <td > 22</td > </tr > <tr > <td > Chloe</td > <td > Princess</td > <td > 85</td > </tr > </tbody > </table >
现在 Pug 的所有功能都可以正常工作,包括模板继承和内联 JavaScript。来我们的演示站点 看看例子。
打包 所以我们完成了在一天内改变 MVC 模板引擎这个目标,不过还剩了一点时间,所以我们可以更进一步来创建一个 NuGet 包。这里有一些挑战,具体涉及在 NuGet 包中包含所需的 node 模块。Simon 计划为此单独写一篇文章。
你可以自己试一试,添加对 pugzor.core
NuGet 包的引用,然后在 Startup.ConfigureServices
方法中的.AddMvc()
后调用.AddPugzor()
。
1 2 3 4 5 public void ConfigureServices (IServiceCollection services ){ services.AddMvc().AddPugzor(); }
Razor 仍会作为默认的模板引擎,但是如果找不到 Razor 视图文件,MVC 框架将会尝试使用 PugzorViewEngine,如果能找到相匹配的 pug 模板,这个模板就会被渲染。 我们在这个项目上做了初次尝试,虽然这是一次比较傻的练习,但是以一些有用的东西作收尾。我们真的很惊讶,为 MVC 创建一个新的模板引擎是这么容易。不过我们不希望 Pugzor 会受到广泛欢迎,但是既然它能够工作,我们希望把它放在那里并看看人们的想法。
我们有一些问题和一些关于扩展 PugzorViewEngine 的想法,让我们知道你的想法或是直接来参与贡献代码。我们接受 Pull Requests :-)