如何组织较大项目的MVC文件夹结构

现在还用不到,拷贝下来备用,原文链接
2016 年 9 月
第 31 卷,第 9 期

ASP.NET Core - ASP.NET Core MVC 的功能切分

作者 Steve Smith | 2016 年 9 月 | 获取代码

大型 Web 应用比小型 Web 应用需要更好的组织。在大型应用中,ASP.NET MVC(和 Core MVC)所用的默认组织结构开始成为你的负累。你可以使用两种简单的技术来更新组织方法并及时跟进不断增长的应用程序。

Model-View-Controller (MVC) 模式相当成熟,即使在 Microsoft ASP.NET 空间中亦是如此。第一版 ASP.NET MVC 在 2009 年推出,并且在今年夏初全面启动了 ASP.NET Core MVC 平台。至今,随着 ASP.NET MVC 的改进,默认项目结构已保持不变:“控制器”和“视图”的文件夹,通常还有“模型”(或可能是 ViewModels)的文件夹。实际上,如果你现在新建 ASP.NET Core 应用,你会看到这些文件夹是由默认模板创建的,如图 1 中所示。

如何组织较大项目的MVC文件夹结构
图 1 默认 ASP.NET Core Web 应用模板结构

该组织结构具有很多优点。它很熟悉;如果你在过去的几年中一直使用 ASP.NET MVC 项目,你会很快识别它。它井然有序;如果你正在寻找某个控制器或视图,你会很清楚从何处着手。当你开始进行一个新项目时,该组织结构运行良好,因为还没有很多文件。但是,随着项目的增加,有关定位所需控制器或在这些层中的不断增长的文件和文件夹数目中查看文件的摩擦也随之增加。

若要明白我的意思,请设想你在这种相同的结构中组织你的计算机文件。你并没有不同项目或工作类型的单独文件夹,而是只有完全由文件类型所组织的目录。可能有针对文本文档、PDF、图像和电子表格的文件夹。当执行包含多种文档类型的特殊任务时,你将需要在不同的文件夹之间来回跳动并在每个文件夹中的很多文件(这些文件与当前任务并不相关)中来回滚动和搜索。这正是以默认方式组织的 MVC 应用中功能的使用方式。

该方法的问题在于,通过类型(而非通过目的)组织的文件组往往缺少聚合。聚合是指一个模块的元素共同所属的程度。在典型的 ASP.NET MVC 项目中,给定的控制器将参考一个或多个相关视图(在与控制器的名称相对应的文件夹中)。控制器和视图都将参考与控制器的责任相关的一个或多个 ViewModel。但是,通常情况下,ViewModel 类型或视图很少被多种控制器类型使用(且通常情况下,域模型或持久性模型被移至其自己的单独项目)。

示例项目

请考虑一个简单的项目(管理 4 个松散并相关的应用程序概念的任务): Ninjas、Plants、Pirates 和 Zombies。实际示例仅允许你列出、查看并添加这些概念。但是,请设想在这里有涉及更多视图的其他复杂性。该项目的默认组织结构看上去应类似于图 2

如何组织较大项目的MVC文件夹结构
图 2 使用默认组织的示例项目

若要使用包含 Pirates 的一些新功能,你需要导航到“控制器”并查找 PiratesController,然后从“视图”依次导航到 Pirates 和相应的视图文件。尽管只有 5 个控制器,但可以看到有很多上下移动的文件夹导航。当项目的根包含更多文件夹时,此问题更加严重,因为“控制器”和“视图”并非按字母顺序临近彼此排列(因此其他文件夹往往会在文件夹列表中的这两个文件夹间放置)。

通过文件类型组织文件的替代方法是按应用程序执行的行来对其进行组织。你的项目将围绕功能或组织的区域来组织文件夹,以替代按控制器、模型和视图组织的文件夹。当对应用的某个特定功能相关的 bug 或功能进行操作时,你需要将很少的文件夹处于打开状态,因为相关文件可能被一起存储。有多种方式可以实现此操作,包括对功能文件夹使用内置 Areas 功能和使用你自己的约定。

ASP.NET Core MVC 如何查看文件

我们有必要抽出一些时间来谈谈 ASP.NET Core MVC 如何与其使用的应用程序内置的标准类型文件一起工作。应用程序的服务器端中所涉及的大部分文件都将在某些 .NET 语言中分类编写。只要它们可以通过应用程序编译和引用,这些代码文件即可存在于磁盘上的任何位置。具体来说,控制器类文件无需存储在任何特定文件夹中。各种模型类(域模型、视图模型、持久性模型等)都是相同的,它们都可以轻松地存在于 ASP.NET MVC Core 项目的单独的项目中。你可以对应用程序中的大部分代码文件按你想要的任何方式进行排列和重新排列。

但是,“视图”文件是不同的。“视图”文件是内容文件。它们相对于应用程序的控制器类所存储的位置是不相关的,但是 MVC 需要知道在哪里可以找到它们,这一点很重要。与默认的“视图”文件夹相比,Areas 提供对不同区域中定位视图的内置支持。你也可以对 MVC 确定视图位置的方式进行自定义。

使用 Areas 组织 MVC 项目

Areas 提供在 ASP.NET MVC 应用程序内组织独立模块的方式。每个 Area 都具有一个模拟项目根约定的文件夹结构。因此,你的 MVC 应用程序应具有相同的根文件夹约定和称为 Areas 的额外文件夹,其中包含一个应用的每个部分的文件夹,它包括“控制器”和“视图”的文件夹(根据需要,可能还包括“模型”或“ViewModels”文件夹)。

Areas 具有强大的功能:允许你将某一大型应用程序细分为单独且逻辑上合理的不同的子应用程序。例如,控制器可以具有跨区域相同的名称,实际上,在应用程序内的每个区域中具有 HomeController 类是很常见的。

若要添加对 ASP.NET MVC Core 项目的 Areas 的支持,只需新建一个名为“Areas”的根级文件夹。在该文件夹中,为你想要在 Area 内组织的应用程序的每个部分新建一个文件夹。然后,在该文件夹内,为“控制器”和“视图”新添文件夹。

因此,你的控制器文件应位于:

复制
/Areas/[area name]/Controllers/[controller name].cs

你的控制器需具有对其适用的 Area 属性,以使框架知道它们属于某个特定区域内:

复制
namespace WithAreas.Areas.Ninjas.Controllers
{
  [Area("Ninjas")]
  public class HomeController : Controller

然后,你的视图应位于:

复制
/Areas/[area name]/Views/[controller name]/[action name].cshtml

应更新已移至区域中的视图的任何链接。如果你正在使用标记帮助程序,则可以将区域名称指定为标记帮助程序的一部分。例如,

复制
<a asp-area="Ninjas" asp-controller="Home" asp-action="Index">Ninjas</a>

同一区域内的视图间的链接可省略 asp-­area 属性。

需要对支持你的应用中的区域所做的最后一件事是,在“配置”方法中对 Startup.cs 中的应用程序的默认路由规则进行更新:

复制
app.UseMvc(routes =>
{
  // Areas support
  routes.MapRoute(
    name: "areaRoute",
    template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
  routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");
});

例如,管理各种 Ninjas、Pirates 等的示例应用程序可以利用 Areas 来实现项目组织结构,如图 3 中所示。

如何组织较大项目的MVC文件夹结构
图 3 使用 Areas 组织 ASP.NET Core 项目

相对于默认约定,Areas 功能通过为应用程序的每个逻辑部分提供单独的文件夹提供了改进。Areas 是 ASP.NET Core MVC 中的内置功能,它需要最少的设置。如果你还未使用它们,请记得它们是将你的应用的相关部分组合在一起并从应用的剩余部分分离的一个简单的方法。

但是,Areas 组织的文件夹仍然负荷很重。你可以在所需的垂直空间中看到这一点,它显示了 Areas 文件夹中相对较小的文件数量。如果你的每个区域并没有很多控制器,且你的每个控制器中并没有很多视图,则上面的这个文件夹带来的麻烦与使用默认约定几乎是相同的。

幸运的是,你可以轻松地创建你自己的约定。

ASP.NET Core MVC 中的功能文件夹

在默认文件夹约定或内置 Areas 功能使用以外,组织 MVC 项目最热门的方法是使用每个功能的文件。对已在垂直细分中采用了交付功能的团队尤其如此(参阅 bit.ly/2abpJ7t),因为大部分垂直细分的用户界面关注点可以存在于任一功能文件夹中。

通过功能(而非通过文件类型)组织你的项目时,通常会有一个根文件夹(如“功能”),其中会有每个功能的子文件夹。这与组织 areas 的方法非常相似。但是,在每个功能文件夹内,你将包括所有所需的控制器、视图和 ViewModel 类型。在大部分应用程序中,这会导致文件夹中有 5 到 15 个项目,所有这些项目都紧密相关。功能文件夹的整个内容都可以保留在解决方案资源管理器中,以供查看。你可以在图 4 中看到有关此示例项目的组织的示例。

如何组织较大项目的MVC文件夹结构
图 4 功能文件夹组织

请注意,即使根级别“控制器”和“视图”文件夹也被消除了。现在,应用的主页位于名为“主页”的其自己的功能文件夹中,而共享文件(如 _Layout.cshtml)也位于“功能”文件夹内的“共享”文件夹。该项目组织结构扩展性很好,使开发人员在进行应用程序的某个特定部分时,可以将其注意力集中在更少的文件夹上。

在此示例中,与使用 Areas 的不同之处在于,并不需要控制器的其他路由和属性(但需要注意的是,该控制器名称必须在该实施中的功能间是唯一的)。若要支持此组织,需要自定义 IViewLocationExpander 和 IControllerModelConvention。将两者与一些自定义 ViewLocationFormats 一起使用,以配置你的“启动”类中的 MVC。

对于给定的控制器,了解它与什么功能关联是很有用的。Areas 通过使用属性来实现此目的,而该方法使用约定。约定设定控制器位于名为“功能”的命名空间中,而在“功能”后的命名空间层中的下一项是功能名称。该名称被添加到属性(在视图位置过程中可用),如图 5 所示。

图 5 FeatureConvention: IControllerModelConvention
复制
{
  public void Apply(ControllerModel controller)
  {
    controller.Properties.Add("feature",
GetFeatureName(controller.ControllerType));
  }
  private string GetFeatureName(TypeInfo controllerType)
  {
    string[] tokens = controllerType.FullName.Split('.');
    if (!tokens.Any(t => t == "Features")) return "";
    string featureName = tokens
      .SkipWhile(t => !t.Equals("features",
        StringComparison.CurrentCultureIgnoreCase))
      .Skip(1)
      .Take(1)
      .FirstOrDefault();
    return featureName;
  }
}

在启动中添加 MVC 时,将该约定添加为 MvcOptions 的一部分:

复制
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

若要将 MVC 使用的正常视图位置逻辑替换为基于功能的约定,你可以清除由 MVC 使用的 View­LocationFormats,并将其替换为你自己的列表。该操作作为 AddMvc 调用的一部分执行,如图 6 中所示。

图 6 替换由 MVC 使用的正常视图位置逻辑
复制
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()))
  .AddRazorOptions(options =>
  {
    // {0} - Action Name
    // {1} - Controller Name
    // {2} - Area Name
    // {3} - Feature Name
    // Replace normal view location entirely
    options.ViewLocationFormats.Clear();
    options.ViewLocationFormats.Add("/Features/{3}/{1}/{0}.cshtml");
    options.ViewLocationFormats.Add("/Features/{3}/{0}.cshtml");
    options.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml");
    options.ViewLocationExpanders.Add(new FeatureViewLocationExpander());
  }

默认情况下,这些格式字符串包含操作的占位符(“{0}”)、控制器(“{1}”)和区域(“{2}”)。该方法添加功能的第 4 个标记(“{3}”)。

使用的视图位置格式应支持在功能内具有相同名称但由不同控制器使用的视图。例如,在功能中具有多个控制器,并且多个控制器具有一个索引方法,这是很常见的。通过在与控制器名称匹配的文件夹中搜索视图支持该功能。因此,NinjasController.Index 和 SwordsController.Index 应在各自的 /Features/Ninjas/Ninjas/Index.cshtml 和 /Features/Ninjas/Swords/Index.cshtml 中定位视图(参见图 7)。

如何组织较大项目的MVC文件夹结构
图 7 每个功能的多个控制器

请注意,该功能是可选的 - 如果你的功能没有必要区分视图(例如,因为功能仅有一个控制器),则只需将视图直接置入功能文件夹中。同样,相较于文件夹,你更愿意使用文件前缀,则只需轻松地将格式字符串从“{3}/{1}”调整为“{3}{1}”以供使用,从而产生视图文件名,如 NinjasIndex.cshtml 和 SwordsIndex.cshtml。

功能文件夹的根中和共享子文件夹中均支持共享视图。

IViewLocationExpander 界面提供了一个 ExpandViewLocations 方法(框架使用该方法识别包含视图的文件夹)。当操作返回视图时将搜索这些文件夹。该方法仅需要 ViewLocation­Expander 将“{3}”标记替换为控制器的功能名称(由前面提到的 FeatureConvention 指定):

复制
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
  IEnumerable<string> viewLocations)
{
  // Error checking removed for brevity
  var controllerActionDescriptor =
    context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
  string featureName = controllerActionDescriptor.Properties["feature"] as string;
  foreach (var location in viewLocations)
  {
    yield return location.Replace("{3}", featureName);
  }
}

若要正确支持发布,你还需要更新 project.json 的 publishOptions 以包括“功能”文件夹:

复制
"publishOptions": {
  "include": [
    "wwwroot",
    "Views",
    "Areas/**/*.cshtml",
    "Features/**/*.cshtml",
    "appsettings.json",
    "web.config"
  ]
},

使用名为“功能”的文件夹的新约定以及文件夹在其中的组织方式完全由你控制。通过修改 View­LocationFormats 集(或者 FeatureViewLocationExpander 类型的行为),你可以完全控制你的应用的视图所处的位置,这是重新组织你的文件唯一需要的操作,因为控制器类型无论位于哪个文件夹中均会被发现。

并行功能文件夹

如果想要与默认 MVC Area 和视图约定并行尝试功能文件夹,只需很小的修改即可实现此功能。将功能格式插入列表起始位置来替代清除 ViewLocationFormats(注意顺序是颠倒的):

复制
options.ViewLocationFormats.Insert(0, "/Features/Shared/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{1}/{0}.cshtml");

若要支持与区域组合的功能,则对 AreaViewLocationFormats 集合也进行修改:

复制
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{1}/{0}.cshtml");

如何处理模型?

目光敏锐的读者将会注意到,我并没有将我的模型类型移入功能文件夹(或 Areas)。在该示例中,我没有单独的 ViewModel 类型,因为我正在使用的模型极其简单。在实际的应用中,你的域或持久性模型所具有的复杂性可能超过你的视图所需的复杂性,此种情况将在单独项目中由其自己定义。你的 MVC 应用可能会定义仅包含给定视图所需数据的 ViewModel 类型,针对显示进行了优化(或由客户端的 API 请求使用)。毫无疑问,这些 ViewModel 类型应置于它们被使用的功能文件夹中(这些类型在功能间被共享的情况应当是很少见的)。

总结

示例包括 NinjaPiratePlant­Zombie 组织者应用程序的所有三个版本,并支持添加和查看每种数据类型。下载示例(或在 GitHub 上查看该示例)并思考每种方法在你现在所使用的应用程序的上下文中的运行方式。试验将 Area 或功能文件夹添加到你所使用的一个大型应用程序中,并确定与使用基于文件类型的*文件夹相比,你是否更愿意将功能细分作为你的应用的文件夹结构的*组织使用。

此示例的源代码可通过 bit.ly/29MxsI0 获取。


Steve Smith 是独立的培训师、导师、顾问,同时也是 ASP.NET MVP。他为 ASP.NET Core 官方文档 (docs.asp.net) 撰写了许多文章,并帮助团队快速掌握了 ASP.NET Core。你可通过 ardalis.com 与他联系。


衷心感谢以下技术专家对本文的审阅: Ryan Nowak
Ryan Nowak 是供职于 Microsoft ASP.NET 团队的开发人员。

上一篇:Class类文件的结构


下一篇:container injection——容器技术