从 MVC 到 Razor Pages






4.86/5 (22投票s)
在 ASP.NET Core、Razor pages、React JS.NET 和 EF Core 上构建一个小型电子商务网站
引言
随着 ASP.NET Core 2 的发布,微软为我们提供了一种全新的替代 MVC (Model-View-Controller) 方法来创建 Web 应用程序。微软将其命名为 Razor Pages,尽管它是一种不同的方法,但在某些方面仍然很熟悉。
本文将展示一个使用 Razor Pages 来制作一个小型电子商务网站的场景,同时借助 Facebook 的 React / ReactJS.NET 来为其提供基于 JavaScript 代码的视图渲染/绑定引擎。
引用免责声明:本文主要关注 Razor Pages,并补充了我几个月前发表的 React Shop - 一个小型电子商务网站 一文。如果您想了解更多关于在 ASP.NET 应用程序中使用 React 和 React JS.NET 的信息,请参阅该篇文章。
安装
为了运行附加源代码中的应用程序,请安装以下软件:
- .NET Core 2.0.0 SDK 或更高版本。
- Visual Studio 2017 版本 15.3 或更高版本,并安装 ASP.NET 和 Web 开发 工作负载。
- Microsoft® SQL Server® 2014 Express 或更高版本。
- 请在本文顶部下载项目源代码。
背景
如果您从过去十年就开始关注 ASP.NET 的发展,您可能已经注意到微软时不时会以新的开发工具、框架甚至新的设计模式的形式进行创新。其中一些已被证明相当强大和可靠,比如 ASP.NET MVC 和 Razor 视图引擎,而另一些则未能经受住时间的考验而被放弃,比如 AJAX Control Toolkit。
但自 2016 年 ASP.NET Core 1.0 发布以来,情况发生了很大变化。微软重写了 ASP.NET,使其成为一个开源、托管在 GitHub 上的项目,并首次为其 Web 开发框架引入了跨平台支持。
从第一个版本开始,ASP.NET Core 就为我们提供了标准的 Web 开发框架,该框架自 ASP.NET 3.0 以来已被广泛采用——即基于模型-视图-控制器(Model-View-Controller)设计模式的 ASP.NET MVC。
MVC 设计模式最早于 1979 年在富有传奇色彩的施乐帕洛阿尔托研究中心(XPARC)开发出来,但直到多个 Web 框架开始采用它后才变得著名:2002 年的 Java Spring,2005 年的 Django 和 Ruby on Rails,以及 2009 年的 ASP.NET。
ASP.NET MVC 在我们的社区中大获成功。在此之前,我们不得不处理 ASP.NET Web Forms 带来的问题,如 Update Panels、不断增长的 ViewState、Postbacks 和相当复杂的页面事件管理。有了 ASP.NET MVC,我们被教导在开发 Web 应用程序时要使用关注点分离的语言:数据属于模型(Model),表示逻辑仅用于视图(View),每个请求都必须由控制器(Controller)的操作(action)处理,控制器进而决定渲染哪个视图及其相应的模型。再也不需要向程序员隐藏 Web 的无状态特性了。
但是,尽管 ASP.NET MVC 给我们带来了很多好处,但有时它仍然会受到一些批评。虽然用户——当然还有 Web 开发人员——基本上将 Web 应用程序视为一组“网页”,但 ASP.NET MVC 并没有一个清晰的网页概念。相反,在 ASP.NET MVC 项目中,每个组件通常都有自己的文件,并根据其在 MVC 框架中的角色属于不同的文件夹。
现在,请停下来想一想你是如何组织电脑文件夹的。想象一下,你有一些不同的工作要做,涉及到不同的文件,而你的电脑文件夹不是按主题、工作或话题来组织的,而是按文件类型来组织的。再想象一下,你正在同时做多项不同的工作,但不是按每个工作来分组文件,而是将每个工作的文件分散在像这样的不同文件夹中:
这会是个好主意吗?
同样,我们可以从下图中看到,一个典型的 MVC 项目是如何将单个页面的组件保存在分散于多个文件和文件夹中的一组文件中的。
因此,在 MVC 中,没有单个的“网页”文件。向刚接触这项技术的人解释这一点有点尴尬。
然后微软的某个人认为同样的事情可以用不同的方式来完成。
介绍 Razor Pages
如果你拿一个 MVC 应用程序,然后把你的视图(View)称为页面(Page)(例如,在 Index.cshtml 文件中),并且你不仅集中了模型(Model)数据,还把与该页面相关的服务器端代码(过去在你的控制器中)集中到一个专用于该页面的类中(在一个 Index.cshtml.cs 文件中)——你现在称之为页面模型(Page Model),那会怎么样?
如果你曾经从事过原生移动应用的开发,那么你可能在模型-视图-视图模型(MVVM)模式中看到过类似的东西。
Razor Pages 中的页面和页面模型
Razor Page 就是这样诞生的!ASP.NET Core 2.0——更确切地说是 Visual Studio 2017——引入了一个新的 Web 应用程序模板,使用 Razor Page 作为默认内容。
然后你可能会想:“等等……这看起来有点太熟悉了”。因为现在你有一个页面和一个执行服务器端功能的类。Page Model 不就是旧的 code behind 文件吗?这不又是 Web Forms 了吗?
不。让我解释一下为什么 Razor Pages 不是 Web Forms 的复兴。首先,我们必须认识到 Razor Page 与 MVC 设计模式没有太大区别。过去,使用 Web Forms 时,你通常会将业务规则、用户界面和数据层混为一谈。而在 ASP.NET Web Forms 中,你有那种人为的管道机制,以牺牲简单性、性能和带宽为代价来实现事件处理;另一方面,MVC 的所有组件在 Razor Pages 中或多或少都是可见的。它们只是被放置在不同的类/文件/文件夹中,以方便页面的开发。
有些人对 Razor Pages 的另一个误解是,它主要适用于初级开发人员或不太复杂的应用程序。虽然新手开发人员理解 Razor Pages 可能比理解 MVC Web 应用更容易,这是事实,但你仍然能够构建复杂的应用程序,正如我们将在本文附带的源代码中看到的那样。
你可能还会认为,一旦创建了一个新的 Razor Pages Web 应用程序,你就以某种方式“失去”了使用 MVC 项目的能力。但幸运的是,事实并非如此。请记住,两种模板(MVC 和 Razor Pages)都依赖于相同的 ASP.NET Core MVC 框架。因此,你可以在同一个项目中,例如,创建一个新的 Razor Page 项目,然后创建 MVC 文件夹(Controllers、Views 等)和所需的文件,以便与 MVC 控制器和视图一起工作!
在 Razor Pages 项目中,在项目根目录下创建一个新的 Controllers 文件夹,然后创建一个 TestController
类。现在我们实现 Index
操作来返回纯文本。
public class TestController : Controller
{
public IActionResult Index()
{
//Accessible through /Test
return Content("You can work with MVC Controllers and Views " +
"alongside Razor Pages in the same project!");
}
}
运行应用程序并在浏览器地址栏中输入 https://:XXXXX/test,我们得到:
这不是很棒吗?
配置
当你创建一个新的 ASP.NET Core Razor Pages Web 应用时,这是你在 Program.cs 文件中得到的内容:
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
Program.cs 文件
现在,请注意 .UseStartup<Startup>()
这一行。它告诉 ASP.NET Core 指定 Web 主机要使用的 Startup 类。
当你选择 Razor Pages 模板时,Startup 类 也会被自动创建。它带有一个 ConfigureServices 方法,用于向 Web 应用添加服务,以及一个 Configure 方法,用于配置 HTTP 请求管道。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
}
Startup.cs 文件
上面的代码显示了 ConfigureServices 方法调用了 MVC 服务 services.AddMvc();(看到 Razor Pages 是如何依赖 MVC 框架的吗?)此外,Configure 方法由运行时调用以设置请求管道。从上面的代码中,我们看到 Configure 方法:
- 指定错误页面配置
- 为当前请求路径启用静态文件服务
- 将 MVC 添加到 Microsoft.AspNetCore.Builder.IApplicationBuilder 请求执行管道中
就是这样。就像 ASP.NET Core MVC 应用程序一样,Razor Pages Web 应用程序也使用 Startup 类来配置 Web 应用程序的所有内容。
新的 Razor Page Web 应用程序
还记得我们说过 Razor Pages 就像是以不同方式实现的 ASP.NET Core MVC 吗?尽管与 MVC 不同,Razor Pages 仍然依赖于 ASP.NET Core MVC 框架。一旦你使用 Razor Pages 模板创建一个新项目,Visual Studio 就会通过 `Startup.cs` 文件配置应用程序以启用 ASP.NET Core MVC 框架,正如我们刚才所见。
该模板不仅为 MVC 使用配置了新的 Web 应用程序,还创建了 Page 文件夹以及一套用于示例应用程序的 Razor 页面和页面模型。
对于 Razor Page Shop Web 应用程序,我需要 3 个不同的视图(页面):
- 产品目录:用户将在此从目录中选择要放入购物车的产品。
- 购物车:为采购订单选择的产品。
- 订单详情:刚下订单的配送数据、客户信息和产品列表。
因此,我保留了 Index.cshtml 作为产品目录页面,并增加了另外两个 Razor Pages 页面:Cart.cshtml 和 CheckoutSuccess.cshtml,分别用于购物车和订单详情。
两个新的 Razor Pages:Cart.cshtml 和 CheckoutSuccess.cshtml。
Pages 文件夹
上图显示了每个视图(嗯,Razor Page)现在都包含在 Pages 文件夹中。新的 Pages 文件夹与传统 MVC Web 应用的 “Views” 文件夹之间的区别不仅仅在于文件夹名称。事实上,由于我们在 Razor Page Web 应用中没有控制器和操作的概念,cshtml 文件在 Pages 文件夹中的位置本身就定义了应该通过哪个 URL 路由来访问它。例如:
- /Pages/Index.cshtml -> "/" 或 "/Index"
- /Pages/Cart.cshtml -> "/Cart"
- /Pages/CheckoutSuccess.cshtml -> "/CheckoutSuccess"
同样,你可能想在 Pages 文件夹内创建子文件夹,以便创建更复杂的 URL 路由方案:
- /Pages/Products/WhatsNew.cshtml -> "/Products/WhatsNew"
- /Pages/Categories/Listing.cshtml -> "/Categories/Listing"
- /Pages/Admin/Dashboard.cshtml -> "/Admin/Dashboard"
Razor Page 的剖析
乍一看,一个 Razor Page 很像一个普通的 ASP.NET MVC 视图文件。但是一个 Razor Page 需要一个新的指令。每个 Razor Page 都必须以 `@page` 指令开始,它告诉 ASP.NET Core 将其视为 Razor page。下图展示了一个典型 Razor Page 的更多细节。
@page - 将文件标识为 Razor Page。没有它,页面将无法被 ASP.NET Core 访问。
@model - 很像在 MVC 应用程序中,定义了绑定数据的来源类,以及页面请求的 Get/Post 方法。
@using - 用于定义命名空间的常规指令。
@inject - 配置应将哪个接口的实例注入到页面模型类中。
@{ } - Razor 括号内的一段 C# 代码,在本例中用于定义页面标题。
<div…> - 与启用了 Razor 的 C# 代码一起出现的常规 HTML 代码。
Web 开发变得简单
在 Razor Pages 应用中创建静态页面怎么样?假设你必须创建一个使用条款页面。
在 Razor Pages 之前,使用常规的 MVC Web 应用程序,你必须遵循一些步骤才能在你的 Web 应用程序中包含一个简单的静态页面:
- 添加一个控制器 (Controllers/TermsOfUseController.cs)
- 添加一个 Action 方法 (Index())
- 添加一个视图文件夹 (/Views/TermsOfUse)
- 添加一个视图 (/Views/TermsOfUse/Index.cshtml)
现在,有了 Razor pages,你的工作变得简单多了:
- 在 (Pages/TermsOfUse.cshtml) 添加一个页面
现在你可能想知道:上面提到的使用条款页面的页面模型(Page Model)呢?由于该页面只是一个静态页面,它不需要任何页面模型,这是相对于 MVC Web 应用程序的另一个优势。
页面模型类
与通常绑定到自定义 `Model` 或 `ViewModel` 的 MVC 视图不同,典型的 Razor Page 会将其 `Model` 指定为一个继承自 `PageModel` 类的类。请看下面的 `IndexModel` 类:
public class IndexModel : PageModel
{
public IList<ProductDTO> Products { get; set; }
readonly ICheckoutManager _checkoutManager;
public IndexModel(ICheckoutManager checkoutManager)
{
this._checkoutManager = checkoutManager;
}
public void OnGet()
{
Products = this._checkoutManager.GetProducts();
}
public async Task<IActionResult> OnPostAddAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToPage("Cart");
}
}
Index.cshtml.cs 文件
上述页面模型通常放在一个 .cshtml.cs 文件中,有时被称为“代码隐藏”文件。但请不要将其与过去 Web Forms 中臭名昭著的“代码隐藏”文件混淆。它们几乎没有共同之处。
现在看一下 `IndexPageModel` 的构造函数。
readonly ICheckoutManager _checkoutManager;
public IndexModel(ICheckoutManager checkoutManager)
{
this._checkoutManager = checkoutManager;
}
你可能想知道是谁用 `ICheckoutManager checkoutManager` 参数调用这个构造函数。对我们来说幸运的是,从 ASP.NET Core 1 开始,该框架就内置了依赖注入服务。
依赖注入的概念是,某个类的实例必须由框架自动创建,但该类的构造函数依赖于另一个接口的实例。因此,在第一个类(本例中为 `IndexModel`)被实例化之前,框架应创建任何依赖项(本例中为 `ICheckoutManager` 接口),然后将它们传递给构造函数。
但为了从接口创建实例,框架需要知道哪些类实现了这些接口。你应该在 Startup.cs 文件中配置这个依赖注入信息,更确切地说,是在 `ConfigureServices` 方法中。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ICheckoutManager, CheckoutManager>();
.
.
.
在 Startup.cs 文件中配置依赖注入。
请注意 `services.AddScoped<ICheckoutManager, CheckoutManager>();` 这行代码是如何将 `CheckoutManager` 类定义为在需要 `ICheckoutManager` 的新实例时应创建的类型。这是通过 `services.AddScoped` 方法完成的。
说到配置,请注意我们使用了 `AddScoped` 方法。ASP.NET Core 为我们提供了一些关于这些实例应该多久创建一次以及它们应该被用于依赖注入多少次的选项。这被称为服务生命周期,这些生命周期可以配置如下:
Transient(瞬时):每次请求时都会创建一个新实例。
Scoped(作用域):每个请求创建一次新实例。
Singleton(单例):在第一次请求时创建一个新实例,并且在每个后续请求中都使用相同的实例。
除了 `Startup.cs` 的配置,你还应该在页面文件中包含一个 `@inject` 指令,指明哪些接口应该由依赖注入服务在请求时生成。
@inject ICheckoutManager ICheckoutManager;
配置 @inject 指令
介绍页面处理程序
如果你习惯于使用 MVC 模式,你可能会想知道 Razor Pages 是如何取代 MVC 操作方法的。在 MVC Web 应用中,控制器是应用程序中每个请求的入口点。在 MVC 模式中,控制器是许多操作的集合,这些操作可能响应应用程序中的不同视图,并为共享资源(如过滤器和路由模板)而方便地组合在一起。
Razor Pages 现在引入了页面处理程序(Page Handlers),它们是 MVC 控制器操作的替代品。这些处理程序响应特定的 HTTP 动词,如 GET 和 POST。这些处理程序存在于 Razor Pages 中,并遵循 "on{HTTP Verb}" 的命名约定。
这意味着每个针对 Razor Page 发出的 HTTP Get 请求都会落到“代码隐藏”文件中的 Page Model 的 `OnGet` 方法上。例如,在 `Index` Razor Page 中,我们有一个 `OnGet` 处理程序方法,用于实例化 Bootstrap 产品轮播中显示的产品列表。
public void OnGet()
{
Products = this._checkoutManager.GetProducts();
}
但如果页面模型没有这样的 OnGet 方法,或者页面模型根本不存在呢?幸运的是,在这种情况下,Razor Page 仍然会被调用,并且不会出错。这是一个方便的解决方案,避免了为简单的 Razor Pages 编写繁琐的样板代码。
现在,如果你想处理 HTTP Post 请求,比如那些由 HTML `<form>` 元素提交的请求,例如:
<form method="post">
.
.
.
<button type="submit" class="btn btn-link" name="SKU" value="@product.SKU">
<i class="fa fa-shopping-cart" aria-hidden="false"></i>
Add to Cart
</button>
.
.
.
</form>
那么你应该遵循 "on{HTTP Verb}" 的命名约定,并创建一个名为 `OnPostAsync` 的新 Razor 页面处理程序(方法)。
public async Task<IActionResult> OnPostAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToPage("Cart");
}
还有另一种情况,你可能希望在一个 Razor Page 内为不同目的提交不同的 HTTP POST 请求。例如,你可能有 3 个按钮:一个用于添加,一个用于更新,另一个用于删除操作。你应该如何在一个 `OnPostAsync` 处理程序中处理不同的 POST 请求呢?
幸运的是,在这种情况下,Razor Pages 提供了一个方便的替代方案,即使用标签助手(Tag Helpers)。标签助手,正如在 ASP.NET Core 1.0 中首次看到的那样,使服务器端代码能够参与创建和渲染 Razor 文件中的 HTML 元素。通过标签助手,你可以指定在同一页面中使用的多个 POST 处理程序方法。例如,假设你想将你的方法名从 `OnPostAsync` 更改为 `OnPostAddAsync`。
public async Task<IActionResult> OnPostAddAsync(string SKU)
{
this._checkoutManager.SaveCart(new CartItemDTO
{
SKU = SKU,
Quantity = 1
});
return RedirectToPage("Cart");
}
显然,之前的 `<form>` HTML 将无法向新重命名的处理程序方法提交请求。但你可以将 `<form>` HTML 元素更改为标签助手,并使用 asp-page-handler 来为 POST 请求指定新的处理程序变体。
<form asp-page-handler="Add"
.
.
.
<button type="submit" class="btn btn-link" name="SKU" value="@product.SKU">
<i class="fa fa-shopping-cart" aria-hidden="false"></i>
Add to Cart
</button>
.
.
.
</form>
将 ReactJS.NET 集成到 ASP.NET Core 2.0 中
上次我使用 React 和 ASP.NET 4.x 时,我遵循了 React 的服务器端渲染指南,通过 .jsx 文件配置了 `ReactConfig` 静态类,这些文件包含了每个视图的内容。
public static class ReactConfig
{
public static void Configure()
{
ReactSiteConfiguration.Configuration
.AddScript("~/Scripts/showdown.js")
.AddScript("~/Scripts/react-bootstrap.js")
.AddScript("~/Scripts/Components.jsx")
.AddScript("~/Scripts/Cart.jsx")
.AddScript("~/Scripts/CheckoutSuccess.jsx");
}
}
旧的 ReactConfig.cs 配置文件
但那个教程是为 ASP.NET 4.x 编写的,在 ASP.NET Core Web 应用中不起作用。所以我不得不将服务器端渲染部分移植到 ASP.NET Core 的 Startup.cs 文件中,遵循新的 React 教程。因此,我必须在 `Startup` 类的 `Configure` 方法中包含那些脚本,以便 ReactJS.NET 能够从 .jsx 文件中渲染服务器端的 HTML 内容。
app.UseReact(config =>
{
config
.AddScript("~/lib/react-bootstrap/react-bootstrap.min.js")
.AddScript("~/js/Components.jsx")
.AddScript("~/js/Cart.jsx")
.AddScript("~/js/CheckoutSuccess.jsx")
.SetUseDebugReact(true);
});
新的 Startup 配置文件中的 Configure 方法
但是 React.AspNet 扩展还要求你调用 `services.AddReact();` 方法,以便注册 ReactJS.NET 所需的所有服务。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddScoped<ICheckoutManager, CheckoutManager>();
services.AddReact();
.
.
.
}
应用程序页面
由于这个 Web 应用程序的页面是从另一个 MVC 应用程序的视图移植到这个 Razor Pages Web 应用程序的,并且在我另一篇文章 React Shop - 一个小型电子商务网站 中已经详细讨论过,而且它们基本保持不变,所以我选择不再重新解释所有内容。如果你希望深入了解有关 React-Bootstrap 组件、React JS 视图以及在 ASP.NET 应用程序中使用 .jsx 文件的细节,请访问我的另一篇文章。
尽管如此,这里还是对应用程序页面做一个简单的概述。
产品目录
产品目录页面。
产品目录页面显示一个简单的产品轮播控件。这个 Bootstrap 控件对于显示无限动画非常有用,并且可以用一小部分页面空间展示许多产品。
产品轮播是通过 Razor 视图引擎在服务器端渲染的。轮播一次显示四个产品,因此 Index.cshtml 视图中的代码定义了一个 `foreach` 循环,该循环遍历每 4 个产品组成的“页面”。
购物车
购物车页面。
与产品目录相比,购物车页面的渲染方式非常不同。首先,Razor 引擎不直接用于渲染视图。相反,Razor 调用 React.Web.Mvc.HtmlHelperExtensions 类的 `React` 方法,并将模型传递给它。然后,Razor 渲染已声明为 React 组件的 `CartView`。
结账详情
结账成功页面。
尽管 `CheckoutSuccess` 页面没有任何交互(除了“返回产品目录”按钮),但它完全是作为一个单一的 React 组件实现的。原因是我们能够利用 React-Bootstrap 库组件提供的简单语法,我们已经在另一篇文章中解释过。所有的绑定值都通过 props 传递,无需使用 React 的 state 对象。
结论
我希望这篇文章能帮助你快速入门这个 Razor Pages 的新世界。我相信你会发现许多场景中,Razor Pages 可能是传统 ASP.NET MVC 应用程序的一个诱人替代方案。
非常感谢您的时间和耐心!如果您有任何建议,请告诉我。别忘了在下面的评论区留下您的意见。
历史
- 2017/10/01 - 初始版本。