ASP.NET Core 的替代视图引擎:接入 ASP.NET Core 架构
ASP.NET Core 是一个优秀的开源 Web 应用开发框架。它的优势之一在于其模块化架构,这使得我们可以用其他组件替换许多默认组件。本文将介绍如何使用 ASP.NET 创建和使用新的视图引擎。
创建 Razor 视图
我们首先创建一个带有 Razor 视图的 ASP.NET Core MVC 应用程序。您需要.NET Core SDK。使用操作系统终端或您喜欢的 IDE(例如VS Code,一款优秀的跨平台 IDE),创建一个名为 Foo 的项目,并使用 MVC 应用程序的默认模板对其进行初始化。
mkdir foo
cd foo/
dotnet new mvc
现在您可以运行该应用程序dotnet run并导航至https://localhost:5001/主页以访问并显示欢迎消息。
现在我们将向 Home 控制器添加一个名为 Bar 的操作,以及相应的 Razor 视图。
编辑HomeController.cs并添加以下内容:
public IActionResult Bar(){
ViewData["Message"] = "Hello World!";
return View();
}
创建Views/Home/Bar.cshtml并添加以下内容:
Your Message: @ViewData["Message"]
Your Message: Hello World!如果你停止并再次运行该应用,你应该会在访问时看到该文本。https://localhost:5001/Home/Bar
另一种视图引擎
正如我们上面看到的,ASP.NET 自带的 Razor 视图引擎使用扩展名为 .rb 的标记文件.cshtml。Razor 语法使用 \rb@符号来切换到 C# 并计算表达式。作为替代方案,如果我们使用 Mustache 语法来创建标记文件呢?我们可以使用大括号来表示需要作为 C# 表达式处理的变量。在我们的例子中,上面的 Bar 视图在新模板系统中将如下所示:
Your Message: {{Message}}
我们将采用与 Razor 类似的约定来定位视图——Views/[controller]/[action]即使用文件扩展名.stache。接下来,我们来看看如何实现 Stache 视图引擎。
创建视图引擎
视图引擎需要实现两个接口:` view_engine_namespace`IViewEngine和IView`view_engine_namespace` ,它们都位于 `<namespace>` 命名空间Microsoft.AspNetCore.Mvc.ViewEngines中。在我们的项目中,创建一个名为 `<top-level-directory>` 的顶级目录,Stache其中包含 `view_engine_namespace`StacheViewEngine.cs和StacheView.cs`view_engine_namespace` 文件。下面我们将详细介绍这两个接口的关键部分。您可以在此 GitHub gist上找到这两个文件的完整内容。
视图引擎
IViewEngine需要实现两个方法:
public ViewEngineResult GetView (string executingFilePath, string viewPath, bool isMainPage);
public ViewEngineResult FindView (ActionContext context, string viewName, bool isMainPage);
两种方法都返回一个ViewEngineResult包含 的ViewEngineResult.Found,表示找到了视图的实例,以及ViewEngineResult.NotFound,表示没有找到视图,并包含搜索位置的列表。
讨论如何引用从控制器返回的视图有助于理解这两个方法在视图渲染过程中的作用。在我们的 Bar 控制器中,我们只需编写 `getView ()` 方法return View();,它引用了与操作同名的视图(例如 `getView()` Bar)。我们也可以直接命名视图:`getView()` return View("Bar");。或者,也可以显式地提供视图文件的完整路径:`getView()` return View("~/Views/Home/Bar.stache");。在所有情况下,框架都会首先调用 `getView()`GetView()方法。如果找不到视图,则会调用 `getView()` 方法FindView()。
如果明确指定了视图路径,我们期望GetView()返回该视图的实例。如果只有视图名称,我们会回退到FindView()我们预先定义的各个路径中搜索,这些路径都是视图可能存在的路径。
在我们的实现中GetView(),我们首先检查它是否viewPath是实际的文件路径,还是仅仅是操作的名称。如果视图仅通过名称引用,则我们会得到后者。在这种情况下,我们将返回,ViewEngineResult.NotFound以便调用可以继续进行FindView()。
如果是文件路径,我们会将其传递给一个私有方法,GetAbsolutePath()以确保其格式正确。executingFilePath只有当存在另一个正在执行的视图时(例如,处理 Razor 局部视图时的父视图),该路径才会有值。如果有值,它会与视图路径合并,生成相对于正在执行的父视图的路径。获得路径后,我们会检查该路径上是否存在模板,并返回相应的结果。
public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
{
if(string.IsNullOrEmpty(viewPath) || !viewPath.EndsWith(ViewExtension, StringComparison.OrdinalIgnoreCase)){
return ViewEngineResult.NotFound(viewPath, Enumerable.Empty<string>());
}
var appRelativePath = GetAbsolutePath(executingFilePath, viewPath);
if(File.Exists(appRelativePath)){
return ViewEngineResult.Found(viewPath, new StacheView(appRelativePath));
}
return ViewEngineResult.NotFound(viewPath, new List<string>{ appRelativePath});
}
在我们的实现中FindView(),我们获得了视图名称以及操作上下文,从中我们可以获取控制器名称。根据我们定位视图的约定,我们将这两个值代入每个_viewLocationFormats(例如Views/[controller]/[action].stache),并检查模板文件,直到找到为止。如果没有找到,我们将返回ViewEngineResult.NotFound所有检查过的路径。
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
if(context.ActionDescriptor.RouteValues.TryGetValue("controller", out var controllerName)){
var checkedLocations = new List<string>();
foreach(var locationFormat in _viewLocationFormats){
var possibleViewLocation = string.Format(locationFormat, viewName, controllerName);
if(File.Exists(possibleViewLocation)){
return ViewEngineResult.Found(viewName, new StacheView(possibleViewLocation));
}
checkedLocations.Add(possibleViewLocation);
}
return ViewEngineResult.NotFound(viewName, checkedLocations);
}
throw new Exception("Controller route value not found.");
}
伊维
IView需要实现该Path属性和RenderAsync()方法。Path是视图模板的位置。我们在实例化视图时填充该Path属性。GetView()FindView()
该RenderAsync方法负责根据给定的参数渲染视图ViewContext,也是实现所有功能的关键所在。在这个极其简化的示例中,我们只需在模板中查找字符串{{Message}}并将其替换为参数中提供的值ViewData。在更完善的版本中,自定义语法解析器将在此处发挥作用。
public Task RenderAsync(ViewContext context)
{
var template = File.ReadAllText(Path);
var processedOutput = template.Replace("{{Message}}", context.ViewData["Message"]?.ToString());
return context.Writer.WriteAsync(processedOutput);
}
使用替代视图引擎
Views/Home/Bar.stache让我们使用新的 Stache 模板语法创建一个备选的 Bar 视图。创建包含以下内容的文件:
Your Message: {{Message}}
最后一步是告诉 MVC 使用新的视图引擎。Startup.cs修改 MVC 配置如下:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddViewOptions(options => {
options.ViewEngines.Insert(0, new StacheViewEngine());
});
请注意,我们将 Stache 视图引擎添加为列表中的第一项。这将确保在找到给定视图的 `<script>`.cshtml和.stache`<script>` 标记文件时,使用 Stache 模板。如果您希望默认使用 Razor 视图,可以将 Stache 添加到列表末尾:options.ViewEngines.Add(new StacheViewEngine());。
如果您运行该应用程序并导航到“工具栏”操作https://localhost:5001/Home/Bar,您现在应该可以看到使用 Stache 渲染的视图!
概括
在上面的例子中,我们看到用其他视图引擎替换 Razor 相对简单。这说明在 ASP.NET 中,即使同时使用多种标记模板也是可行的。
虽然我们已经有了可以继续发展的基础,但这个简单的例子相当有限:
- 我们只能绑定到一个名为 `<variable_name>` 的变量
Message。对于给定的模板,我们应该能够使用任意数量的变量。 - 我们需要支持更高级的表达式,而不仅仅是变量(例如内联表达式,如
{{1 + 1}}),循环遍历列表,以及 if-else 条件来控制流程。 - 我们缺少 Razor 的一些更高级的概念,例如布局、局部视图和标签助手。
探索这些缺失的功能并开发我们的替代视图引擎来实现它们,将会很有趣。或许我们会在以后的文章中探讨这个问题。
感谢@sohjsolwin对本文的反馈。
还要特别感谢 Dave Paquette 的博文“在 ASP.NET Core 中创建新的视图引擎”,它为本文提供了一些关键信息。
封面照片由Eryk通过Unsplash提供。