Главная страница   /   24.4. Использование связок скриптов и стилей (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

24.4. Использование связок скриптов и стилей

Первым делом мы объединим наши файлы JavaScript и CSS в связки, что позволит работать с ними как с одним элементом. Связки определяются в файле /App_Start/BundleConfig.cs. В листинге 24-показано содержимое этого файла по умолчанию, которое создается Visual Studio.

Листинг 24-5: Содержимое файла BundleConfig.cs по умолчанию
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 вызывается из метода Application_Start файла Global.asax при первом запуске приложения MVC Framework. Метод RegisterBundles принимает объект BundleCollection, в котором мы регистрируем новые связки файлов с помощью метода Add.

Подсказка

Классы, которые используются для создания связок, содержатся в пространстве имен System.Web.Optimization; во время написания этой книги документацию MSDN API для этого пространства имен было не так легко найти. Чтобы узнать больше о классах этого пространства имен, воспользуйтесь ссылкой http://msdn.microsoft.com/en-us/library/system.web.optimization.aspx.

Мы можем создавать связки для скриптов и таблиц стилей, но имейте в виду, нельзя объединять эти типы файлов, так как MVC Framework оптимизирует их по-разному. Стили представлены в классе StyleBundle, скрипты – в классе ScriptBundle.

Чтобы создать новую связку, нужно создать экземпляр либо StyleBundle, либо ScriptBundle, оба из которых принимают единственный аргумент конструктора – это маршрут для ссылки на связку. Этот маршрут будет использоваться в качестве URL, по которому браузер будет запрашивать содержимое связки; поэтому важно использовать схему маршрута, чтобы он не конфликтовал с другими маршрутами в приложении. Самый безопасный способ это сделать - начать маршрут с ~/bundles или ~/Content. (Вы поймете, как это важно, когда мы объясним принцип работы связок).

Когда вы создали объект StyleBundle или ScriptBundle, добавьте в связку информацию о таблицах стилей и скриптах с помощью метода Include. Есть несколько интересных функций, которые позволяют сделать связки более гибкими. Чтобы продемонстрировать это, мы отредактировали и упростили связки нашего приложения в файле BundleConfig.cs, как показано в листинге 24-6. Мы добавили одну новую связку, отредактировали одну из существующих и удалили все те, которые нам не нужны.

Листинг 24-6: Настраиваем конфигурацию связок
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"));
		}
	}
}

Сначала мы изменили связку StyleBundle с маршрутом ~/Content/css. Мы хотим, чтобы эта связка содержала все файлы CSS в нашем приложении, поэтому мы изменили аргумент, переданный в метод Include, с ~/Content/site.css (который ссылается на один файл) на ~/Content/*.css. Символ звездочки (*) является универсальным, что означает, что теперь в нашей связке есть ссылка на все файлы CSS из папки /Content нашего проекта. Так можно гарантировать, что все файлы в папке будут автоматически включены в связку и порядок, в котором эти файлы будут загружены, не будет иметь значения. Так как для нас порядок загрузки CSS файлов не важен, мы можем использовать универсальный символ, но если вы будете полагаться на правила предшествования стилей CSS, то вам необходимо перечислить файлы по отдельности в определенном порядке.

Мы также добавили в файл BundleConfig.cs связку ScriptBundle с маршрутом ~/bundles/clientfeaturesscripts. Вскоре вы опять увидите маршруты для этих связок, когда мы будем применять их в приложении. Для этой связки мы использовали метод Include, в котором указали отдельные файлы JavaScript, разделенные запятыми, потому что нам нужны только некоторые файлы из папки Scripts и для нас важен порядок, в котором браузер загружает и выполняет код.

Обратите внимание, как мы указали файл библиотеки jQuery:

~/Scripts/jquery-{version}.js

В имени файла очень удобно использовать фрагмент {version}, потому что в таком случае он будет соответствовать любой версии указанного файла и в зависимости от конфигурации приложения выберет либо обычную, либо минимизированную версию файла. MVC 4 содержит версию библиотеки jQuery 1.7.1, что означает, что наша связка будет включать файл /Scripts/jQuery-1.7.1.js в процессе разработки и /Scripts/jQuery-1.7.1.min.js при развертывании.

Подсказка

Выбор между обычной и минимизированной версией осуществляется на основе узла compilation в файле Web.config. Обычная версия будет использоваться, если атрибут debug содержит значение true, а минимизированная - если атрибут debug содержит false. Далее в этой главе мы переключим режим приложения с отладки на развертывание, и вы сможете увидеть, как это происходит.

Преимущество использования {version} заключается в том, что вы можете обновить библиотеки до последних версий и не определять связки заново. Недостатком является то, что {version} не сможет различить две версии одной и той же библиотеки в одном каталоге. Так, например, если мы добавим файл jQuery-1.7.2.js в папку Scripts, то клиент загрузит файлы 1.7.1 и 1.7.2. Чтобы это не подорвало нашу оптимизацию, мы должны убедиться, что в папке /Scripts находится только одна версия библиотеки.

Подсказка

MVC Framework достаточно умен, чтобы игнорировать файлы IntelliSense при обработке {version} в связке, но мы всегда проверяем то, что запрашивает браузер, чтобы не включить нежелательные файлы. Вскоре мы покажем вам, как это сделать.

Применяем связки

Чтобы применить связки, сначала необходимо подготовить представление. Хотя этот шаг не является обязательным, он позволит MVC Framework выполнить максимальную оптимизацию нашего приложения. Мы создали новую папку /Scripts/Home и добавили в него файл JavaScript под названием MakeBooking.js. По соглашению, мы сохраняем файлы JavaScript для отдельных страниц в папках, названных по контроллеру. Содержимое файла MakeBooking.js показано в листинге 24-7.

Листинг 24-7: Содержимое файла MakeBooking.js
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();
	});
});

Это тот же самый код, который использовался ранее - мы только переместили его в отдельный файл. Следующим шагом будет изменение представления /Views/Home/MakeBooking.cshtml: мы удалим элементы link и script, для которых была создана связка, как показано в листинге 24-8. Мы хотим, чтобы браузер запрашивал только необходимые файлы, и если мы оставим эти элементы, запросы будут дублироваться. Единственный оставшийся элемент script ссылается на специфичный для данного представления файл MakeBooking.js, который мы создали в листинге 24-7.

Листинг 24-8: Удаляем элементы link и script из представления MakeBooking.cshtml
@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>

К связкам можно обращаться в файлах представлений, но мы, как правило, создаем связки только для контента, который является общим для нескольких представлений; это означает, что мы применяем связки в файлах макетов. В листинге 24-9 показан файл /Views/Shared/_Layout.cshtml, который добавила в проект Visual Studio.

Листинг 24-9: Макет, добавленный в проект Visual Studio
<!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>

Связки добавляются с помощью вспомогательных методов @Scripts.Render и @Styles.Render. Как видите, макет уже содержит три связки, которые выделены жирным шрифтом. Два вызова @Scripts.Render объясняют некоторые исходные данные профиля. Браузер запрашивает две копии библиотеки jQuery из-за связки ~/bundles/jQuery. Из-за связки ~/bundles/modernizr браузер запрашивает библиотеку, которую мы не используем в приложении.

В самом деле, используется только связка ~/Content/css, потому что она загружает файл /Content/Site.css, который был изначально определен в связке, и все файлы CSS в папке /Content в соответствии с изменениями, которые мы сделали в листинге 24-9. Чтобы создать необходимое нам поведение, мы отредактировали файл _Layout.cshtml, чтобы использовать наши новые связки и удалить ненужные ссылки, как показано в листинге 24-10.

Листинг 24-10: Добавляем нужные связки в файл _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/clientfeaturesscripts")

	@RenderSection("scripts", required: false)
</body>
</html>

Чтобы увидеть HTML, который генерируют эти вспомогательные методы, запустите приложение, перейдите по ссылке /Home/MakeBooking и просмотрите исходный код страницы. Вот вывод метода Styles.Render для связки ~/Content/css:

<link href="/Content/CustomStyles.css" rel="stylesheet"/>
<link href="/Content/Site.css" rel="stylesheet"/>

А вот вывод метода Scripts.Render:

<script src="/Scripts/jquery-1.7.1.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

Осталось еще одно изменение. Выполнение нашего специфического для представления JavaScript, который содержится в файле /Scripts/Home/MakeBooking.js, зависит от jQuery, которая должна создать обработчик события для кнопки. Следовательно, мы должны гарантировать, что файл jQuery загружается перед MakeBooking.js. Если вы посмотрите на макет в листинге 24-10, то увидите, что вызов метода RenderBody находится перед вызовом метода Scripts.Render, а это значит, что элемент script в представлении появляется перед элементами script в макете, и код кнопки не будет работать. (Он либо не создаст оповещения, либо сообщит об ошибке JavaScript, в зависимости от используемого браузера).

Это можно исправить, переместив вызов Scripts.Render в элемент head в представлении; по сути, так мы обычно и делаем. Тем не менее, мы можем также использовать дополнительную секцию scripts, которая определена в файле _Layout.cshtml и которую вы можете увидеть в листинге 24-10. В листинге 24-11 показано, как мы обновили представление MakeBooking.cshtml, чтобы использовать эту секцию.

Листинг 24-11: Используем дополнительную секцию scripts в представлении
@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>

Секция scripts появляется после вызова Scripts.Render в макете, что означает, что наш специфический для представления скрипт не будет загружен до jQuery, и наш элемент button будет работать так, как мы планировали. При использовании связок эта ошибка возникает очень часто, и именно поэтому мы явно ее продемонстрировали.

Измеряем эффект изменений

Мы определили собственные связки, удалили нежелательные ссылки на JavaScript и в целом упорядочили используемые скрипты и таблицы стилей. Сейчас настало время, чтобы измерить эффект изменений и оценить разницу.

Для этого мы очистили кэш браузера, перешли по ссылке /Home/MakeBooking и отследили запросы, которые были отправлены браузером, с помощью утилит F12. Результат показан на рисунке 24-3.

Рисунок 24-3: Измеряем производительность приложения, в котором используются связки

Вот краткая информация о производительности:

  • Браузер сделал 8 запросов по ссылке /Home/MakeBooking.
  • Было отправлено 2 запроса к файлам CSS.
  • Было отправлено 5 запросов к файлам JavaScript.
  • В общей сложности 2,638 байт было отправлено от браузера к серверу.
  • В общей сложности 326,549 байт было отправлено от сервера к браузеру.

Это не плохо. Мы уменьшили объем данных, загружаемый браузером, примерно на 30 процентов. Но результаты станут еще лучше, если мы переключим приложение из режима отладки на развертывание. Для этого необходимо установить атрибуту debug в элементе compilation файла Web.config значение false, как показано в листинге 24-12.

Листинг 24-12: Отключаем режим отладки в файле Web.config
<system.web>
	<httpRuntime targetFramework="4.5" />
	<compilation debug="true" targetFramework="4.5" />

Когда это изменение внесено, мы перезапустим приложение, очистим кэш браузера и измерим сетевые запросы снова. Результат показан на рисунке 24-4.

Рисунок 24-4: Измеряем производительность приложения, в котором используются связки, в режиме развертывания

Вот краткая информация о производительности:

  • Браузер сделал 4 запроса по ссылке /Home/MakeBooking.
  • Был отправлен только 1 запрос к CSS.
  • Было отправлено 2 запроса к файлам JavaScript.
  • В общей сложности 1,372 байт было отправлено от браузера к серверу.
  • В общей сложности 126,340 байт было отправлено от сервера к браузеру.

Вам может стать интересно, почему уменьшилось число запросов к файлам CSS и JavaScript. Причина в том, что MVC Framework объединяет и минимизирует таблицы стилей и файлы JavaScript в режиме развертывания, так что все содержимое связки может быть загружено с помощью одного запроса. Чтобы увидеть, как это работает, посмотрите на HTML, который создает приложение. Вот код, сгенерированный методом Styles.Render:

<link href="/Content/css?v=6jdfBoUlZKSHjUZCe_rkkh4S8jotNCGFD09DYm7kBWE1" rel="stylesheet"/>

А вот код, сгенерированный методом Scripts.Render:

<script src="/bundles/clientfeaturesscripts?v=SOAJUpvGwNr0XsILBsLrEwEpdyIziIN9frqxIjgTyWE1">
</script>

Эти длинные URL используются, чтобы запросить содержимое связки в одном блоке данных. MVC Framework минимизирует данные CSS не так, как файлы JavaScript, поэтому мы должны объединять таблицы стилей и скрипты в разных связках.

Проведенная оптимизация производит значительный эффект. Браузер отправляет гораздо меньше запросов, что уменьшает объем данных, передаваемых клиенту. И мы передаем меньше данных обратно на сервер; в самом деле, мы отправили около 27% от того объема данных, который был зафиксирован при первом измерении производительности в начале этой главы.

На этом мы завершим оптимизацию запросов. Можно было бы добавить файл MakeBooking.js в связку, устранить еще один запрос и уменьшить количество кода; но мы достигли точки, где отдача начинает уменьшаться, и мы начинаем смешивать контент, специфический для представления, с кодом макета, чего лучше избегать. Если бы у нас было более сложное приложение, можно было бы создать вторую связку скриптов, в которую поместить больше пользовательского кода, но наше приложение очень простое и здесь решающим правилом оптимизации будет знать, где остановиться. В этом приложении мы достигли этой точки.