在建立ASP.NET MVC專案時,預設會建立一些專案目錄與檔案,而這些是MVC基本的目錄架構。MVC的專案意即Model, View, Controller各自分離,當然在目錄分類上也不例外,另外在JS Library, Image, Font, CSS都各自獨立目錄,較方便以檔案類型分類。但是當我們改某個頁面的功能時,就要跑去JS, CSS目錄下找與此頁面相關的檔案真的是超麻煩的。像這種時候我們就會想要把同一頁面的相關JS, CSS, HTML放在一起,這樣就違反了MVC的基本目錄,所以今天這篇限量就是要來重整MVC的目錄架構。
原本建立MVC專案後預設的目錄架構會長成像下圖那樣:
因為開發時東找西找實在是太麻煩了,所以限量目標是要將目錄以頁面為主來整理,整理完會像下圖那樣:
首先,我們先把目標目錄按照規則(HTML, CSS, JS以Action的名稱命名放置在同名目錄下)建好,然後執行後就會出現這個錯誤頁面。
這個錯誤意思是系統依預設規則找了這些地方,然後還是沒有找到目標的View,再來就來看看限量如何解決這個問題。
在原本的Action裡 return View() 中加入指定的 View 路徑,修改如下:
這種解法是最不用想太多的方法,只是每次都要自己去指定路徑是有點麻煩。
ASP.NET MVC Dynamic Bundles是別人做的NuGet套件,是一套可改變目錄架構的Plugin,可以解決目前的問題,詳細的設定流程可至 Dynamic Bundles for ASP.NET MVC 研究。
MVC執行時其實是有一個ViewEngine來處理View相關的作業,而預設的ViewEngine為RazorViewEngine。限量用.Net Reflector深入去看看發現在RazorViewEngine定義了去那些位置去搜尋View,程式碼如下:
在MSDN看過RazorViewEngine的資料後知道原來ViewLocationFormats是View的搜尋位置,而且為public代表是可以修改的。考慮到擴充性限量決定用繼承的方式來修改ViewLocationFormats,程式碼如下:
LimitedViewEngine.cs
接著要來找切入點載入LimitedViewEngine。MVC在啟動時,背景程序會載入ViewEngine,因此我們要在Application_Start的切入點載入,因為LimitedViewEngine繼承了RazorViewEngine,我們就不需要有重複的ViewEngine功能,所以要先清空預設載入的ViewEngine然後再加入LimitedViewEngine,加入程式碼如下:
Global.asax.cs
接下來同場加映,限量要來做一下進一步的修改。
MVC Bundle功能提供了一個小撇步可以讓各組Bundle Minify並合併成一個檔案,如下圖:
這個功能只要在Application啟動時加入一段程式碼就可以了:
如果照原本利用section方式在頁面上加入javascript,就算啟用Minify功能也無法讓HTML中的script區段Minify,因為Minify功能是針對.js與.css檔案才可以,如下圖:
在我們修改後的目錄架構中,雖然已經將JS, CSS, HTML都各自抽離,但要使用Minify功能必須要在BundleConfig中綁定後才會有效果,問題來了,我一個頁面綁一個Bundle,那十個頁面就要綁十個,這樣實在沒有好到哪去,所以限量就要來小改造一下。
限量的想法是在BundleConfig載入Bundle時動態遞迴去View目錄底下找JS與CSS並加入Bundle中。然後在頁面載入Layout時去找有沒有該頁面的Bundle,如果有就載入,想法就這麼簡單。實作方法如下:
先在BundleConfig中加入一個DynamicAddBundles方法並呼叫:
完成了! 接下來就來看看執行結果:
執行結果可以看到我們頁面的JS與CSS有載進來且Minify,有沒有非常感動。這樣之後就可以加速開發與Debug速度了。
參考來源:
Matthew Renze - Clean Architecture in ASP.NET MVC 5
CodeProject - Introducing Dynamic Bundles for ASP.NET MVC
MSDN - RazorViewEngine 類別
原本建立MVC專案後預設的目錄架構會長成像下圖那樣:
因為開發時東找西找實在是太麻煩了,所以限量目標是要將目錄以頁面為主來整理,整理完會像下圖那樣:
首先,我們先把目標目錄按照規則(HTML, CSS, JS以Action的名稱命名放置在同名目錄下)建好,然後執行後就會出現這個錯誤頁面。
這個錯誤意思是系統依預設規則找了這些地方,然後還是沒有找到目標的View,再來就來看看限量如何解決這個問題。
解法一. 指定相對相對位置路徑
在原本的Action裡 return View() 中加入指定的 View 路徑,修改如下:
public ActionResult ShoppingCart() { return View("~/Views/Market/ShoppingCart/ShoppingCart.cshtml"); }
這種解法是最不用想太多的方法,只是每次都要自己去指定路徑是有點麻煩。
解法二. 使用ASP.NET MVC Dynamic Bundles
ASP.NET MVC Dynamic Bundles是別人做的NuGet套件,是一套可改變目錄架構的Plugin,可以解決目前的問題,詳細的設定流程可至 Dynamic Bundles for ASP.NET MVC 研究。
解法三. 客製化ViewEngine
MVC執行時其實是有一個ViewEngine來處理View相關的作業,而預設的ViewEngine為RazorViewEngine。限量用.Net Reflector深入去看看發現在RazorViewEngine定義了去那些位置去搜尋View,程式碼如下:
public class RazorViewEngine : BuildManagerViewEngine { internal static readonly string ViewStartFileName = "_ViewStart"; public RazorViewEngine() : this(null) { } public RazorViewEngine(IViewPageActivator viewPageActivator) : base(viewPageActivator) { base.AreaViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" }; base.AreaMasterLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" }; base.AreaPartialViewLocationFormats = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml", "~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" }; base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" }; base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" }; base.PartialViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml", "~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" }; base.FileExtensions = new string[] { "cshtml", "vbhtml" }; } ... }
在MSDN看過RazorViewEngine的資料後知道原來ViewLocationFormats是View的搜尋位置,而且為public代表是可以修改的。考慮到擴充性限量決定用繼承的方式來修改ViewLocationFormats,程式碼如下:
LimitedViewEngine.cs
public class LimitedViewEngine : RazorViewEngine { public LimitedViewEngine() : base() { var viewLocationFormat = new List<string>() { "~/Views/{1}/{0}/{0}.cshtml" }; viewLocationFormat.AddRange(this.ViewLocationFormats); this.ViewLocationFormats = viewLocationFormat.ToArray(); } }
接著要來找切入點載入LimitedViewEngine。MVC在啟動時,背景程序會載入ViewEngine,因此我們要在Application_Start的切入點載入,因為LimitedViewEngine繼承了RazorViewEngine,我們就不需要有重複的ViewEngine功能,所以要先清空預設載入的ViewEngine然後再加入LimitedViewEngine,加入程式碼如下:
Global.asax.cs
protected void Application_Start() { // 清空預設載入的ViewEngines後再加入客製化的ViewEngine ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new LimitedViewEngine()); AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
這樣就完成了,先來執行看看會不會再出現錯誤。
接下來同場加映,限量要來做一下進一步的修改。
MVC Bundle功能提供了一個小撇步可以讓各組Bundle Minify並合併成一個檔案,如下圖:
這個功能只要在Application啟動時加入一段程式碼就可以了:
BundleTable.EnableOptimizations = true;
如果照原本利用section方式在頁面上加入javascript,就算啟用Minify功能也無法讓HTML中的script區段Minify,因為Minify功能是針對.js與.css檔案才可以,如下圖:
在我們修改後的目錄架構中,雖然已經將JS, CSS, HTML都各自抽離,但要使用Minify功能必須要在BundleConfig中綁定後才會有效果,問題來了,我一個頁面綁一個Bundle,那十個頁面就要綁十個,這樣實在沒有好到哪去,所以限量就要來小改造一下。
限量的想法是在BundleConfig載入Bundle時動態遞迴去View目錄底下找JS與CSS並加入Bundle中。然後在頁面載入Layout時去找有沒有該頁面的Bundle,如果有就載入,想法就這麼簡單。實作方法如下:
先在BundleConfig中加入一個DynamicAddBundles方法並呼叫:
///接著再_Layout.cshtml修改如下:/// 動態至指定目錄下搜尋JS與CSS並加入Bundle /// /// 指定目錄 /// Bundle public static void DynamicAddBundles(string virtualPath, BundleCollection bundles) { var vss = BundleTable.VirtualPathProvider; // 針對第一層目錄的所有Entity foreach (VirtualFileBase entity in vss.GetDirectory(virtualPath).Children) { // 如果是資料夾就往裡面搜尋 if (entity.IsDirectory) { DynamicAddBundles(entity.VirtualPath, bundles); } else // 如果是檔案就判斷是否要加入 { // 取得副檔名 var extension = VirtualPathUtility.GetExtension(entity.VirtualPath); // 取得檔案名稱 var fileName = entity.Name.Replace(extension, string.Empty); // 取得檔案的虛擬路徑 var vPath = VirtualPathUtility.ToAppRelative(entity.VirtualPath); if (extension == ".js") // 如果是JS就加入至~/View/JS/XXX的Bundle { bundles.Add(new ScriptBundle(string.Format("~/{0}/Js/{1}", "Views", fileName.ToUpper())) .Include(vPath)); } else if (extension == ".css") // 如果是JS就加入至~/View/Css/XXX的Bundle { bundles.Add(new StyleBundle(string.Format("~/{0}/Css/{1}", "Views", fileName.ToUpper())) .Include(vPath)); } } } } public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); ... // 呼叫動態加入Bundle DynamicAddBundles("~/Views", bundles); }
<!DOCTYPE html> <html> <head> ... @Styles.Render("~/Content/css") @{ // 如果找到該頁面的Css Bundle就加入 if (BundleTable.Bundles.GetBundleFor( string.Concat("~/Views/Css/", ViewBag.Page.ToUpper())) != null) { @Styles.Render(string.Concat("~/Views/Css/", ViewBag.Page.ToUpper())) } } </head> <body> ... @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @{ // 如果找到該頁面的JS Bundle就加入 if (BundleTable.Bundles.GetBundleFor( string.Concat("~/Views/Js/", ViewBag.Page.ToUpper())) != null) { @Scripts.Render(string.Concat("~/Views/Js/", ViewBag.Page.ToUpper())) } } @RenderSection("scripts", required: false) </body> </html>注意:在這邊因為要知道Bundle的名稱才能讀取,所以限量會在View裡設定ViewBag.Page的值,讓程式去找這個值的Bundle,因此要特別注意。
完成了! 接下來就來看看執行結果:
執行結果可以看到我們頁面的JS與CSS有載進來且Minify,有沒有非常感動。這樣之後就可以加速開發與Debug速度了。
參考來源:
Matthew Renze - Clean Architecture in ASP.NET MVC 5
CodeProject - Introducing Dynamic Bundles for ASP.NET MVC
MSDN - RazorViewEngine 類別
留言
張貼留言