捆綁包(Bundle): 能夠組織和優化CSS以及JavaScript文件,是由視圖和佈局引發瀏覽器向伺服器請求的文件。 顯示模式(Display Mode): 針對不同的設備採用不同的視圖。 理解預設腳本庫 在創建除Empty以外的任一MVC項目時,Visual Studio都會在Scripts文 ...
捆綁包(Bundle):
能夠組織和優化CSS以及JavaScript文件,是由視圖和佈局引發瀏覽器向伺服器請求的文件。
顯示模式(Display Mode):
針對不同的設備採用不同的視圖。
理解預設腳本庫
在創建除Empty以外的任一MVC項目時,Visual Studio都會在Scripts文件夾中添加一組JavaScript庫,最主要並常用的有:
- jquery-1.8.2.js:jQuery庫,可使得在瀏覽器中操作HTML元素變得簡單而容易,與HTML標準部分的內建API相比,其優勢尤其明顯。
- jquery-ui-1.8.24.js:jQuery UI庫,通過HTML元素創建富用戶控制項,為Web應用程式創建美觀的UI,該庫建立在jQuery庫之上。
- jquery.mobile-1.1.0.js:jQuery Mobile庫,為移動設備創建富用戶控制項。jQuery Mobile建立在jQuery之上,且只會添加到使用Mobile模板選項創建的項目中。
- jquery.validate.js:jQuery Validation庫,執行HTML表單元素的輸入驗證。
- knockout-2.2.0.js:Knockout庫,將“模型-視圖-視圖模型”模式運用於Web程式的客戶端部分,作用是將Web程式中客戶端的數據從顯示給用戶的元素中分離出來。通常被稱為MVC的瀏覽器。
- modernizr-2.6.2.js:Modernizr庫,用於檢測瀏覽器對HTML5及CSS3的支持情況,能夠在支持情況下使用最新功能,而在不支持時可以優雅降級。
以下是Visual Studio及MVC專用庫:
- jquery-1.8.2.intellisense.js:在視圖中編寫jQuery代碼時,為Visual Studio提供智能感應的功能。
- jquery.unobtrusive-ajax.js:提供MVC框架漸近式Ajax特性,依賴於jQuery。
- jquery.validate-vsdoc.js:在編寫使用jQuery驗證庫的代碼時,為Visual Studio提供智能感應的功能。
- jquery.validate.unobtrusive.js:提供MVC框架漸近式驗證特性,依賴於jQuery。
對於Visual Studio及MVC專用的庫,不需要我們做任何事情,Visual Studio會自動使用它們。
這裡列出的都是常規的版本,同時出現的還會有壓縮版——一般在發佈的時候使用,可以節省很多空間,並減少網路帶寬,節約網路資源。
準備示例
項目:ClientFeatures
項目模板:Basic(基本)
模型類:Appointment.cs
using System; using System.ComponentModel.DataAnnotations; namespace ClientFeatures.Models { public class Appointment { [Required] public string ClientName { get; set; } [DataType(DataType.Date)] public DateTime Date { get; set; } public bool TermsAccepted { get; set; } } }
控制器:Home
using ClientFeatures.Models; using System; using System.Web.Mvc; namespace ClientFeatures.Controllers { public class HomeController : Controller { public ViewResult MakeBooking() { return View(new Appointment { ClientName = "Adam", Date = DateTime.Now.AddDays(2), TermsAccepted = true }); } public JsonResult MakeBooking(Appointment appt) { // 在實際項目中,這裡是存儲新 Appointment 的語句 return Json(appt, JsonRequestBehavior.AllowGet); } } }
視圖:MakeBooking.cshtml
@model ClientFeatures.Models.Appointment @{ ViewBag.Title = "Make A Booking"; AjaxOptions ajaxOpts = new AjaxOptions { OnSuccess = "processResponse" }; } <h2>Book an Appointment</h2> <link rel="stylesheet" href="~/Content/CustomStyles.css" /> <script src="~/Scripts/jquery-1.8.2.min.js"></script> <script src="~/Scripts/jquery.validate.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.js"></script> <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script> <script type="text/javascript"> function processResponse(appt) { $('#successClientName').text(app.ClientName); $('#successDate').text(processDate(app.Date)); switchViews(); } function processDate(dateString) { return new Date(parseInt(dateString.substr(6, dateString.length - 8))).toDateString(); } function switchViews() { var hidden = $('.hidden'); var visible = $('.visible'); hidden.removeClass('.hidden').addClass('.visible'); visible.removeClass('.visible').addClass('.hidden'); } $(document).ready(function () { $('#backButton').click(function (e) { switchViews(); }) }); </script> <div id="formDiv" class="visible"> @using (Ajax.BeginForm(ajaxOpts)) { @Html.ValidationSummary(true) <p>@Html.ValidationMessageFor(m => m.ClientName)</p> <p>Your name: @Html.EditorFor(m => m.ClientName)</p> <p>@Html.ValidationMessageFor(m => m.Date)</p> <p>Appointment Date: @Html.EditorFor(m => m.Date)</p> <p>@Html.ValidationMessageFor(m => m.TermsAccepted)</p> <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms $ conditions</p> <input type="submit" value="Make booking" /> } </div> <div id="successDiv" class="hidden"> <h4>Your appointment is confirmed</h4> <p>Your name is: <b id="successClientName"></b></p> <p>The date of your appointment is: <b id="successDate"></b></p> <button id="backButton">Back</button> </div>
上面視圖中,兩個div元素,一個會在視圖第一次渲染時顯示給用戶,它含有一個已啟用Ajax的表單。當表單被遞交併在接受到伺服器對Ajax請求作出的相應時,應採取的應對方式是隱藏第一個div中的表單,同時顯示另一個div元素,顯示預約所確定的細節。
視圖中link元素中引用的/Content文件夾中的CustomStyles .css文件為自定義添加的CSS文件,內容如下:
div.hidden { display: none; } div.hidden { display: block; }
此處希望創建一個用於複雜視圖的典型場景,但又不需要創建一個複雜的應用程式,這就是為什麼添加的CSS文件只有兩盒樣式,也是為什麼對一個十分簡單的視圖使用一連串的jQuery庫的原因。其關鍵思想是有許多文件要進行管理。當在編寫一個實際程式時,所受到的考驗恰恰是需要在視圖中處理許多腳本和樣式文件。
現在啟動程式看就可以看到效果了:
管理腳本與樣式表
上面示例中視圖代碼中混用了Scripts文件夾中的庫、Content文件夾中的CSS樣式表、本地的Script元素,還有HTML和Razor標記。但是,按照示例那樣寫還是存在著一些隱含的問題,我們可以將腳本和樣式表進行管理以改善。
腳本及樣式表載入的資料分析
在對一個項目進行優化之前,最好是先做一些測量。對於本例使用的是IE11的“F12工具”進行測量。
啟動程式,導航至/Home/MakeBooking,然後按F12鍵。之後點擊“網路”選項卡( ),點擊“網路流量捕獲”按鈕( )。重載瀏覽器內容(刷新頁面)將會得到如下圖這樣的結果:
下麵列出的是從上圖看到的一些關鍵數據:
- 瀏覽器對Home/MakeBooking形成了9個請求
- 2個請求用於CSS文件
- 6個請求用於JavaScript文件
- 瀏覽器發送給服務的有22322位元組
- 伺服器發送給瀏覽器的有642670位元組
這些都是最壞情況下的數據,因為在載入之前已經清理了緩存。如果在現實中,瀏覽器的緩存未清理,則瀏覽器會通過之前的請求緩衝,對此會有所改善。
如果不清理緩存,再次載入,則會是下麵這種效果:
- 瀏覽器對Home/MakeBooking形成了9個請求
- 2個請求用於CSS文件
- 6個請求用於JavaScript文件
- 瀏覽器發送給服務的有4610位元組
- 伺服器發送給瀏覽器的有158315位元組
如果註意觀察,將會發現視圖下載的JavaScript文件列表中已經重新創建了兩個常見的問題。第一個是混用了最小化的和常規的JavaScript文件。這個問題不大,但對開發期間的調試會造成一定的影響,所以,最好不用混用。
第二個是同時下載了jQuery庫的最小化和常規版本。發送這種情況是因為佈局也會載入一些JavaScript和CSS文件,而用戶缺乏相應的手段強制瀏覽器不用下載它已經擁有的代碼。
後面的內容,將介紹一下如何有控制地獲取腳本好CSS文件。更廣泛意義上,也會展示如何減少瀏覽器需要發送給伺服器的請求數,以及需要下載的數據量。
使用腳本和樣式捆綁包
將JavaScript和CSS文件組織成捆綁包(Bundle),使其能夠作為一個單一的單元進行處理。捆綁包是在/App_Start/BundleConfig.cs文件中定義的。下麵是由Visual Studio預設創建的:
using System.Web; using System.Web.Optimization; namespace ClientFeatures { public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include( "~/Scripts/jquery-ui-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.unobtrusive*", "~/Scripts/jquery.validate*")); bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css")); bundles.Add(new StyleBundle("~/Content/themes/base/css").Include( "~/Content/themes/base/jquery.ui.core.css", "~/Content/themes/base/jquery.ui.resizable.css", "~/Content/themes/base/jquery.ui.selectable.css", "~/Content/themes/base/jquery.ui.accordion.css", "~/Content/themes/base/jquery.ui.autocomplete.css", "~/Content/themes/base/jquery.ui.button.css", "~/Content/themes/base/jquery.ui.dialog.css", "~/Content/themes/base/jquery.ui.slider.css", "~/Content/themes/base/jquery.ui.tabs.css", "~/Content/themes/base/jquery.ui.datepicker.css", "~/Content/themes/base/jquery.ui.progressbar.css", "~/Content/themes/base/jquery.ui.theme.css")); } } }
其中靜態方法RegisterBundles會在MVC程式第一次啟動時,通過Global.asax中的Application_Start方法調用。RegisterBundles方法以一個BundleCollection對象為參數,通過使用它的Add方法註冊新的文件捆綁包。
也可以分別創建用於腳本和樣式表的捆綁包,重要的是將這些文件類型分開,因為MVC框架對這些文件的優化是不同的。樣式是由StyleBundle類表示,而腳本是由ScriptBundle類表示。
創建一個新的捆綁包,實際上就是在創建StyleBundle或ScriptBundle類的一個實例。它們都有一個構造函數參數,即引用捆綁包的路徑。其作用是作為瀏覽器請求捆綁包內容的一個URL,因此,重要的是要為這些路徑使用一個與應用程式所支持的路徑無衝突的URL方案。最安全的做法是以~/bundles或~/Content作為起始路徑。
一旦創建了上述的StyleBundle或ScriptBundle對象,就可以使用Include方法添加捆綁包所包含的樣式或腳本文件的細節。有一些較好的做法,可以參考下麵對BundleConfig類進行的修改:
using System.Web; using System.Web.Optimization; namespace ClientFeatures { public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/*.css")); bundles.Add(new ScriptBundle("~/bundles/clientfeaturesscripts").Include( "~/Scripts/jquery-{version}.js", "~/Scripts/jquery.validate.js", "~/Scripts/jquery.validate.unobtrusive.js", "~/Scripts/jquery.unobtrusive-ajax.js")); } } }
上述修改中,首先使用~/Content/css路徑對StyleBundle進行了修改,並將Include方法的參數改為~/Content/*.css,以能夠使該捆綁包包含程式中所有的CSS文件。文件尾碼.css前面的星號(*)是一個通配符,表示Content文件夾中的所有CSS文件,但這裡忽略了文件的順序,當然這在此處並不重要。——實際上,在瀏覽器中,CSS文件的載入順序是不重要的,因此使用通配符的方式是很好的選擇。但是,如果要使用CSS的樣式優先規則,則需要分別列出這些文件,以保證順序的正確。
ScriptBundle中的路徑設置為~/bundles/clientfeaturesscripts,這個捆綁包中使用Include方法逐一指定了需要的腳本文件,並以逗號分隔,原因是此處只需要部分腳本文件,並關註腳本的載入和執行的順序。
註意jQuery庫文件的指定方式:~/Scripts/jquery-{version}.js,文件名中的{version}部分相對靈活,因為這樣做,會匹配指定文件的任一版本,它會使用程式的配置,選擇該文件的常規或最小化版本。
提示:使用常規版或最小版是由Web.config文件中的compilation元素決定的。當其debug屬性被設置為true時,使用常規版;而當debug為false時,則使用最小化版。
這麼寫的好處是可以將所使用的庫的更新為新版本,而不必重新定義捆綁包。缺點是{version}標誌無法區分同一個文件夾中同一個庫的不同版本。因此,必須確保Scripts文件夾中只有一個版本的庫。
運用捆綁包
使用捆綁包之前,首先要做的是創建視圖。當然也可以沒有這一步,但這可以讓MVC框架能夠為應用程式執行最大限度的優化。
創建一個新的文件夾,路徑及名稱為/Scripts/Home,在該文件夾中添加一個新的腳本MakeBooking.js。這是需要遵守的約定,以便按控制器來組織各個頁面的JavaScript文件(即按控制器名(不含“Controller”部分)及動作方法名組織JavaScript腳本的約定):
function processResponse(appt) { $('#successClientName').text(appt.ClientName); $('#successDate').text(processDate(appt.Date)); switchViews(); } function processDate(dateString) { return new Date(parseInt(dateString.substr(6, dateString.length - 8))).toDateString(); } function switchViews() { var hidden = $('.hidden'); var visible = $('.visible'); hidden.removeClass('hidden').addClass('visible'); visible.removeClass('visible').addClass('hidden'); } $(document).ready(function () { $('#backButton').click(function (e) { switchViews(); }) });
上面代碼只是將原來視圖中的那部分腳本轉移到了獨立的js文件中。接著就是要修改視圖文件了。修改原則是希望瀏覽器只請求其所需要的文件,適當地保留需要負責副本的請求,所以可以刪除已經創建捆綁包的那些link和script元素,保留唯一的一個指向新建的那個專門的js文件MakeBooking.js的那個script元素,具體如下:
@model ClientFeatures.Models.Appointment @{ ViewBag.Title = "Make A Booking"; AjaxOptions ajaxOpts = new AjaxOptions { OnSuccess = "processResponse" }; } <h4>Book an Appointment</h4> <script src="~/Scripts/Home/MakeBooking.js" type="text/javascript"></script> <div id="formDiv" class="visible"> @using (Ajax.BeginForm(ajaxOpts)) { @Html.ValidationSummary(true) <p>@Html.ValidationMessageFor(m => m.ClientName)</p> <p>Your name: @Html.EditorFor(m => m.ClientName)</p> <p>@Html.ValidationMessageFor(m => m.Date)</p> <p>Appointment Date: @Html.EditorFor(m => m.Date)</p> <p>@Html.ValidationMessageFor(m => m.TermsAccepted)</p> <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms $ conditions</p> <input type="submit" value="Make booking" /> } </div> <div id="successDiv" class="hidden"> <h4>Your appointment is confirmed</h4> <p>Your name is: <b id="successClientName"></b></p> <p>The date of your appointment is: <b id="successDate"></b></p> <button id="backButton">Back</button> </div>
可以在視圖文件中引用捆綁包,但最好是創建能夠多個視圖共用的捆綁包——即在佈局中運用捆綁包:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> @RenderBody() @Scripts.Render("~/bundles/jquery") @RenderSection("scripts", required: false) </body> </html>
捆綁包是分別使用@Styles.Render和@Scripts.Render輔助器方法添加的。為了達到預期目標,我們需要編輯_Layout.cshtml文件,以使用新定義的捆綁包,刪除不想使用的引用:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> @Styles.Render("~/Content/css") </head> <body> @RenderBody() @Scripts.Render("~/bundles/clientfeaturesscriptes") @RenderSection("scripts", required: false) </body> </html>
好了,現在可以看看最終生成的HTML,下麵是由用於~/Content/css捆綁包的Styles.Render方法生成的結果:
<link href="/Content/CustomStyles.css" rel="stylesheet"></link>
<link href="/Content/Site.css" rel="stylesheet"></link>
而這些是Scripts.Render方法生成的:
<script src="/Scripts/jquery-1.8.2.js"></script>
<script src="/Scripts/jquery.unobtrusive-ajax.js"></script>
<script src="/Scripts/jquery.validate.js"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js"></script>
使用Scripts小節
到現在為止,該示例還不能很好的工作,原因是包含在專業的js文件MakeBooking.js中的腳本代碼需要依靠jQuery為按鈕建立事件處理程式。也就是,jQuery文件要在MakeBooking.js之前載入,但在佈局中能夠看出實際的載入順序正好相反,導致視圖中的script元素出現在佈局中的script元素之前,導致按鈕不能正常工作,甚至有的瀏覽器還會報出JavaScript錯誤,比如我用的IE11就報出瞭如下圖的錯誤:
解決上面的問題,可以有兩種方式:
1、 將Scripts.Render調用移入視圖的head元素;
2、 利用_Layout.cshtml文件中定義的“可選腳本小節(Optional Scripts Section)”——使用這種方式其實就是定義了一個占位符,當視圖中有可選腳本小節時,便將視圖中的實際代碼放在這兒。
下麵使用的是第二種方式做的修改,請參考:
@model ClientFeatures.Models.Appointment @{ ViewBag.Title = "Make A Booking"; AjaxOptions ajaxOpts = new AjaxOptions { OnSuccess = "processResponse" }; } <h4>Book an Appointment</h4> @section scripts{ <script src="~/Scripts/Home/MakeBooking.js" type="text/javascript"></script> } <div id="formDiv" class="visible"> @using (Ajax.BeginForm(ajaxOpts)) { @Html.ValidationSummary(true) <p>@Html.ValidationMessageFor(m => m.ClientName)</p> <p>Your name: @Html.EditorFor(m => m.ClientName)</p> <p>@Html.ValidationMessageFor(m => m.Date)</p> <p>Appointment Date: @Html.EditorFor(m => m.Date)</p> <p>@Html.ValidationMessageFor(m => m.TermsAccepted)</p> <p>@Html.EditorFor(m => m.TermsAccepted) I accept the terms & conditions</p> <input type="submit" value="Make booking" /> } </div> <div id="successDiv" class="hidden"> <h4>Your appointment is confirmed</h4> <p>Your name is: <b id="successClientName"></b></p> <p>The date of your appointment is: <b id="successDate"></b></p> <button id="backButton">Back</button> </div>
這樣一來,script小節會出現在佈局中對Scripts.Render的調用之後,視圖專用的腳本也就會在jQuery之後才載入,按鈕元素也就能正常工作,且不會再報類似之前那種錯誤了。
這裡還是要提醒一下,在使用捆綁包時,這是經常會出現的錯誤,有必要對其加以重視,這也是為什麼專門把這一問題介紹一下的原因。
註:關於可選腳本小節,可參考“視圖”一章內關於“對Razor視圖添加動態內容”中的“使用分段”小節的介紹,理解分段的概念及其工作原理。
修改後的資料分析
現在清理一下緩存,並導航到/Home/MakeBooking,看看這一調整的效果:
下麵是主要信息的摘要:
- 瀏覽器對/Home/MakeBooking形成了8個請求(主要請求項)
- 2個用於CSS文件的請求
- 5個用於JavaScript文件的請求
- 從瀏覽器發送給服務的內容有23237位元組
- 從伺服器發給瀏覽器的有496183位元組
對比之前的642670位元組,大約減少了1/3。
如果將程式從調試模式切換到部署配置時,將會更明顯,具體做法是將Web.config文件中compilation元素上的debug屬性設置為false即可:
<system.web> … <compilation debug="false" targetFramework="4.5" /> … </system.web>
此時再次重覆上述步驟,將得到如下結果:
以下是摘要:
- 形成的請求為4個
- 1個請求用於CSS
- 2個請求用於JavaScript
- 瀏覽器發送給服務的內容有1288位元組
- 伺服器發送給瀏覽器的內容有126607位元組
這次對CSS和JavaScript文件的請求變少了,原因是MVC框架在部署模式中,將一系列樣式表和JavaScript文件聯繫在一起,並做了最小化,以使一個捆綁包中的內容能夠通過一個單一的請求進行載入。如果查看程式渲染的HTML,就能明白是怎麼實現的了:
Styles.Render方法生成的結果:
<link href="/Content/css?v=6jdfBoUlZKSHjUZCe_rkkh4S8jotNCGFD09DYm7kBWE1" rel="stylesheet"/>
Scripts方法生成的結果:
<script src="/bundles/clientfeaturesscripts?v=KyclumLmAXQGM1-wDTwVUS31lpYigmXXR8HfERBGk_I1"></script>
這些長長的URL以一個單一的資料庫的形式,請求了一個捆綁包的內容。MVC框架對CSS數據所採取的最小化與JavaScript文件是不同的,這是為什麼將樣式表和腳本分別放在不同捆綁包中的原因。
雖然還可以繼續優化,但對於我們這樣的一個簡單的程式,這已經足夠了,比如如果將MakeBooking.js文件也添加到捆綁包中,還可以再消除一個請求,但這已經意義不大了。一開始那種視圖專用的內容與佈局的代碼混合在一起的做法是提倡的,如果在更複雜的系統中,也會會創建第二個捆綁包,讓它包含更多的自定義代碼,但這裡只是一個簡單的示常式序,優化的準則是要清楚到什麼程度為止。——目前,對應這樣的一個示常式序已經達到目標了。
面向移動設備
移動設備和桌面設備有著很大的區別,最容易引起問題的是觸摸交互和受限制的屏幕尺寸。
更多的移動開發方面的知識不是我們這裡關註的,在此,只需要做些瞭解即可。
觀察應用程式
開發一個移動設備的程式最好是從零開始建立一個新項目。但這裡採取的辦法是在原示例項目(ClientFeatures)基礎中添加支持。
首先,需要一個能夠模擬移動端展示的程式的工具,書中推薦的是Opera Mobile Emulator(Opera移動設備模擬器),該模擬器是免費的,可以從www.opera.com/developer/tools/mobile上下載。當然也可以使用Windows Phone、Android以及Blackberry等,但這些往往都很慢,用起來也很痛苦,因為它們都不是僅模擬瀏覽器,而是模擬了整個移動操作系統。
我在學習的時候下載的是“Opera Mobile Classic Emulator 12.1 for Windows”版本。安裝完成後啟動,並將模擬器的設置使用HTC Hera的配置。導航到/Home/MakeBooking將會看到結果如下:
使用移動專用的佈局和視圖
由於程式如此之簡單,以至於沒有什麼醜陋的地方需要修改,只是它並沒有在觸摸屏上以易於操作的方式顯示。後面,簡單介紹如何創建移動專用版本的視圖和佈局,以方便對其內容的調整來適應目標設備。其實,這個過程很簡單,需要做的只是創建其名稱帶有.Mobile的視圖和佈局即可:
視圖:MakeBooking.Mobile.cshtml(註意,在擴展名前面加了.Mobile)
@model ClientFeatures.Models.Appointment @{ ViewBag.Title = "Make A Booking"; AjaxOptions ajaxOpts = new AjaxOptions { OnSuccess = "processResponse" }; } <h4>This is an MOBILE View</h4> @section scripts{ <script src="~/Scripts/Home/MakeBooking.js" type="text/javascript"></script> } <div id="formDiv" class="visible"> @using (Ajax.BeginForm(ajaxOpts)) { @Html.ValidationSummary(true) <p>@Html.ValidationMessageFor(m => m.ClientName)</p> <p>Name: </p><p>@Html.EditorFor(m => m.ClientName)</p><