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

概述

大型 Web 應用比小型 Web 應用須要更好的組織。在大型應用中,ASP.NET MVC(和 Core MVC)所用的默認組織結構開始成爲你的負累。你可使用兩種簡單的技術來更新組織方法並及時跟進不斷增加的應用程序。 html

Model-View-Controller (MVC) 模式至關成熟,即便在 Microsoft ASP.NET 空間中亦是如此。初版 ASP.NET MVC 在 2009 年推出,而且在今年夏初全面啓動了 ASP.NET Core MVC 平臺。至今,隨着 ASP.NET MVC 的改進,默認項目結構已保持不變:「控制器」和「視圖」的文件夾,一般還有「模型」(或多是 ViewModels)的文件夾。實際上,若是你如今新建 ASP.NET Core 應用,你會看到這些文件夾是由默認模板建立的,如圖 1 中所示。 git

圖 1 默認 ASP.NET Core Web 應用模板結構 github

 

該組織結構具備不少優勢。它很熟悉;若是你在過去的幾年中一直使用 ASP.NET MVC 項目,你會很快識別它。它井井有理;若是你正在尋找某個控制器或視圖,你會很清楚從何處着手。當你開始進行一個新項目時,該組織結構運行良好,由於尚未不少文件。可是,隨着項目的增長,有關定位所需控制器或在這些層中的不斷增加的文件和文件夾數目中查看文件的摩擦也隨之增長。 web

若要明白個人意思,請設想你在這種相同的結構中組織你的計算機文件。你並無不一樣項目或工做類型的單獨文件夾,而是隻有徹底由文件類型所組織的目錄。可能有針對文本文檔、PDF、圖像和電子表格的文件夾。當執行包含多種文檔類型的特殊任務時,你將須要在不一樣的文件夾之間來回跳動並在每一個文件夾中的不少文件(這些文件與當前任務並不相關)中來回滾動和搜索。這正是以默認方式組織的 MVC 應用中功能的使用方式。 json

該方法的問題在於,經過類型(而非經過目的)組織的文件組每每缺乏聚合。聚合是指一個模塊的元素共同所屬的程度。在典型的 ASP.NET MVC 項目中,給定的控制器將參考一個或多個相關視圖(在與控制器的名稱相對應的文件夾中)。控制器和視圖都將參考與控制器的責任相關的一個或多個 ViewModel。可是,一般狀況下,ViewModel 類型或視圖不多被多種控制器類型使用(且一般狀況下,域模型或持久性模型被移至其本身的單獨項目)。 服務器

 

示例項目

請考慮一個簡單的項目(管理 4 個鬆散並相關的應用程序概念的任務): Ninjas、Plants、Pirates 和 Zombies。實際示例僅容許你列出、查看並添加這些概念。可是,請設想在這裏有涉及更多視圖的其餘複雜性。該項目的默認組織結構看上去應相似於圖 2app

 

圖 2 使用默認組織的示例項目框架

若要使用包含 Pirates 的一些新功能,你須要導航到「控制器」並查找 PiratesController,而後從「視圖」依次導航到 Pirates 和相應的視圖文件。儘管只有 5 個控制器,但能夠看到有不少上下移動的文件夾導航。當項目的根包含更多文件夾時,此問題更加嚴重,由於「控制器」和「視圖」並不是按字母順序臨近彼此排列(所以其餘文件夾每每會在文件夾列表中的這兩個文件夾間放置)。 優化

經過文件類型組織文件的替代方法是按應用程序執行來對其進行組織。你的項目將圍繞功能或組織的區域來組織文件夾,以替代按控制器、模型和視圖組織的文件夾。當對應用的某個特定功能相關的 bug 或功能進行操做時,你須要將不多的文件夾處於打開狀態,由於相關文件可能被一塊兒存儲。有多種方式能夠實現此操做,包括對功能文件夾使用內置 Areas 功能和使用你本身的約定。 spa

 

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 中所示。

 

圖 3 使用 Areas 組織 ASP.NET Core 項目

 

相對於默認約定,Areas 功能經過爲應用程序的每一個邏輯部分提供單獨的文件夾提供了改進。Areas 是 ASP.NET Core MVC 中的內置功能,它須要最少的設置。若是你還未使用它們,請記得它們是將你的應用的相關部分組合在一塊兒並從應用的剩餘部分分離的一個簡單的方法。

可是,Areas 組織的文件夾仍然負荷很重。你能夠在所需的垂直空間中看到這一點,它顯示了 Areas 文件夾中相對較小的文件數量。若是你的每一個區域並無不少控制器,且你的每一個控制器中並無不少視圖,則上面的這個文件夾帶來的麻煩與使用默認約定幾乎是相同的。

幸運的是,你能夠輕鬆地建立你本身的約定。

 

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

在默認文件夾約定或內置 Areas 功能使用之外,組織 MVC 項目最熱門的方法是使用每一個功能的文件。對已在垂直細分中採用了交付功能的團隊尤爲如此(可參閱 http://deviq.com/vertical-slices/),由於大部分垂直細分的用戶界面關注點能夠存在於任一功能文件夾中。

經過功能(而非經過文件類型)組織你的項目時,一般會有一個根文件夾(如「功能」),其中會有每一個功能的子文件夾。這與組織 areas 的方法很是類似。可是,在每一個功能文件夾內,你將包括全部所需的控制器、視圖和 ViewModel 類型。在大部分應用程序中,這會致使文件夾中有 5 到 15 個項目,全部這些項目都緊密相關。功能文件夾的整個內容均可以保留在解決方案資源管理器中,以供查看。你能夠在圖 4 中看到有關此示例項目的組織的示例。

圖 4 功能文件夾組織

請注意,即便根級別「控制器」和「視圖」文件夾也被消除了。如今,應用的主頁位於名爲「主頁」的其本身的功能文件夾中,而共享文件(如 _Layout.cshtml)也位於「功能」文件夾內的「共享」文件夾。該項目組織結構擴展性很好,使開發人員在進行應用程序的某個特定部分時,能夠將其注意力集中在更少的文件夾上。

在此示例中,與使用 Areas 的不一樣之處在於,並不須要控制器的其餘路由和屬性(但須要注意的是,該控制器名稱必須在該實施中的功能間是惟一的)。若要支持此組織,須要自定義 IViewLocationExpander 和 IControllerModelConvention。將二者與一些自定義 ViewLocationFormats 一塊兒使用,以配置你的「啓動」類中的 MVC。

對於給定的控制器,瞭解它與什麼功能關聯是頗有用的。Areas 經過使用屬性來實現此目的,而該方法使用約定。約定設定控制器位於名爲「功能」的命名空間中,而在「功能」後的命名空間層中的下一項是功能名稱。該名稱被添加到屬性(在視圖位置過程當中可用),如圖 5 所示。

public class 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;
  }
}

圖 5 FeatureConvention: IControllerModelConvention

 

在啓動中添加 MVC 時,將該約定添加爲 MvcOptions 的一部分:

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

若要將 MVC 使用的正常視圖位置邏輯替換爲基於功能的約定,你能夠清除由 MVC 使用的 View­LocationFormats,並將其替換爲你本身的列表。該操做做爲 AddMvc 調用的一部分執行,如圖 6 中所示。

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());
  }

圖 6 替換由 MVC 使用的正常視圖位置邏輯

默認狀況下,這些格式字符串包含操做的佔位符(「{0}」)、控制器(「{1}」)和區域(「{2}」)。該方法添加功能的第 4 個標記(「{3}」)。

使用的視圖位置格式應支持在功能內具備相同名稱但由不一樣控制器使用的視圖。例如,在功能中具備多個控制器,而且多個控制器具備一個索引方法,這是很常見的。經過在與控制器名稱匹配的文件夾中搜索視圖支持該功能。所以,NinjasController.Index 和 SwordsController.Index 應在各自的 /Features/Ninjas/Ninjas/Index.cshtml 和 /Features/Ninjas/Swords/Index.cshtml 中定位視圖(參見圖 7)。

圖 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 或功能文件夾添加到你所使用的一個大型應用程序中,並肯定與使用基於文件類型的頂級文件夾相比,你是否更願意將功能細分做爲你的應用的文件夾結構的頂級組織使用。

此示例的源代碼可經過 https://github.com/smallprogram/OrganizingAspNetCore 獲取。

相關文章
相關標籤/搜索