Сжатие кода JavaScript и CSS

109

В этой статье мы объясним, как работает средство упаковки ASP.NET. Оно появилось в версии ASP.NET 4.5 и помогает упростить управление и сопровождение файлов сценариев JavaScript и таблиц стилей CSS, которые используются приложением. Как будет показано, пакеты могут также применяться при оптимизации запросов, которые браузер должен выполнять для получения файлов сценариев и таблиц стилей.

Пример проекта

Мы создали новый проект под названием ClientDev, используя шаблон ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET) в Visual Studio. В примерах далее будут нужны некоторые пакеты NuGet, поэтому выберите пункт Manage NuGet Packages (Управлять пакетами NuGet) в меню Project (Проект) среды Visual Studio, чтобы открыть окно Manage NuGet Packages (Управление пакетами NuGet). Найдите в категории Online (Онлайновые) и установите следующие пакеты:

Пакеты jQuery и jQuery UI являются библиотеками JavaScript. jQuery UI - это библиотека для построения пользовательских интерфейсов, которая зависит от jQuery. Последний пакет в списке устанавливает средство упаковки ASP.NET, рассматриваемое далее. Для целей этих статей также требуются таблицы стилей. В примере ниже показано содержимое файла MainStyles.css:

div {
    margin-bottom: 10px;
    width: 100%;
    text-align: center;
}

span.message {
    font-family: Arial, sans-serif;
}

Ниже приведено содержимое таблицы стилей ErrorStyles.css:

span.error {
    color: red;
}

Наконец, мы создаем веб-форму по имени Default.aspx с контентом, представленном в примере:

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeBehind="Default.aspx.cs" Inherits="ClientDev.Default" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
    <link rel="stylesheet" href="MainStyles.css" />
    <link rel="stylesheet" href="ErrorStyles.css" />
    <link rel="stylesheet" href="Content/themes/base/all.css" />
    <script src="Scripts/jquery-2.1.3.js"></script>
    <script src="Scripts/jquery-ui-1.11.2.js"></script>
    <script>
        $(document).ready(function () {
            $('input[type=submit]').button();
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <input type="submit" name="color" value="Red" />
            <input type="submit" name="color" value="Green" />
            <input type="submit" name="color" value="Blue" />
        </div>
        <div>
            <span class="message">
                Выбран цвет:
                <span id="selectedValue" runat="server">
                    <span class="error">Ничего не выбрано</span>
                </span>
            </span>
        </div>
    </form>
</body>
</html>

Эта веб-форма содержит три элемента <input>, к которым применен виджет Button из библиотеки jQuery UI, трансформирующий их внешний вид. Элемент <span> серверной стороны используется для отображения сообщения о том, по какой кнопке был совершен щелчок.

Библиотека jQuery UI - это многофункциональный и удобный набор инструментов для построения пользовательских интерфейсов, которым мы пользуемся очень часто и рекомендуем вам взять его на вооружение. Хотя библиотека jQuery UI и применяется в этой статье, поскольку здесь речь идет о зависимостях между библиотеками JavaScript, подробно она рассматриваться не будет. Дополнительные сведения ищите в учебнике по jQuery.

В примере ниже показано, как устанавливается значение элемента <span> серверной стороны в классе отделенного кода:

using System;

namespace ClientDev
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            string selectedColor;
            if (IsPostBack && (selectedColor = Request.Form["color"]) != null)
            {
                selectedValue.InnerText = selectedColor;
            }
        }
    }
}

В этой статье нас больше интересует не элемент <body>, а элемент <head>, который содержит элементы <link> и <script>, требуемые веб-формой. Элементы <link> импортируют таблицу стилей CSS, необходимую для библиотеки jQuery UI, и таблицы стилей, которые были созданы ранее. Первые два элемента <script> загружают библиотеки jQuery и jQuery UI, а последний содержит специальный код, который выбирает элементы <input> и применяет к ним виджет Button из jQuery UI.

Конечный результат можно просмотреть, запустив приложение (веб-форма Default.aspx будет загружена автоматически) и щелкнув на одной из кнопок:

Выбор цветов с использованием веб-формы Default.aspx

Проблемы управления сценариями

Хотя веб-форма Default.aspx проста, она демонстрирует общую проблему, характерную для веб-приложений: необходимость в управлении файлами JavaScript. Мир разработки библиотек JavaScript весьма динамичен, а реальный проект веб-приложения может зависеть он десятков разных библиотек, которые обновляются и выпускаются с различной частотой. Некоторые библиотеки, подобные jQuery, настолько распространены, что от них зависят другие библиотеки - примером может служить библиотека jQuery UI, зависящая от jQuery.

Диспетчер пакетов NuGet может помочь в управлении зависимостями между пакетами, но когда дело доходит до управления элементами <script> в собственных веб-формах, возникают три проблемы, которые описаны в последующих разделах.

Управление версиями файлов JavaScript

Первая проблема касается обеспечения загрузки элементами <script> правильных файлов из папки Scripts. В элементе <head> веб-формы Default.aspx можно заметить проблему, связанную с тем, что элементы <script> ссылаются на конкретные версии файлов библиотек jQuery и jQuery UI:

<script src="Scripts/jquery-2.1.3.js"></script>
<script src="Scripts/jquery-ui-1.11.2.js"></script>

Мы работаем с версией 2.1.3 библиотеки jQuery и версией 1.11.2 библиотеки jQuery UI. Скорее всего, вы будете располагать другими версиями этих библиотек (т.к. они часто обновляются) с отличающимися именами файлов. При обновлении версий используемых библиотек JavaScript, либо вручную, либо с помощью диспетчера пакетов NuGet, придется найти все связанные с ними элементы <script> и модифицировать атрибуты src для ссылки на новые имена файлов - длительная и подверженная ошибкам задача.

Некоторые пытаются избежать этой проблемы, удаляя номера версий из имен файлов JavaScript в проекте. Это решает проблему с атрибутами src, но приводит к отказу работы диспетчера пакетов NuGet, которому не известно, что делать с переименованными файлами.

Управление зависимостями библиотек

Если вы разрабатываете веб-приложение с реалистичной сложностью, то должны уделять внимание порядку, в котором элементы <script> определяются внутри веб-форм. Соблюдение порядка очень важно, потому что элементы <script> необходимо определять так, чтобы они отражали зависимости между библиотеками. В рассматриваемом примере мы используем библиотеку jQuery UI, которая зависит от jQuery. Это означает, что элемент <script>, который загружает файл jQuery, должен находиться перед элементом <script>, загружающим файл jQuery UI; в противном случае библиотека jQuery UI функционировать не будет.

Если вы измените порядок следования элементов <script> на противоположный, то увидите сообщение об ошибке, гласящее о том, что библиотека jQuery не определена; это является верным признаком наличия проблемы с порядком следования, которую необходимо решить.

В JavaScript не хватает практичной системы управления зависимостями, поэтому библиотеки вроде jQuery UI просто предполагают, что библиотека jQuery уже была загружена, и вызывают ее функцию (приводя к ошибке). Существует решение под названием асинхронное определение модулей (asynchronous module definition), поддержка которого неуклонно растет, однако оно еще не достигло той точки, когда его будут поддерживать все популярные библиотеки JavaScript.

Управление минимизацией

Большинство библиотек JavaScript состоят из двух файлов. Первый файл содержит несжатую версию, которую легко читать и можно применять для отладки проблем в коде клиентской стороны. Во втором файле находится сжатая (или минимизированная) версия, имеющая меньший размер. Это не то сжатие, которое осуществляется программами архивации. На самом деле оно предполагает устранение всех пробельных символов и замену значащих имен переменных и функций короткими алфавитно-цифровыми именами.

По соглашению минимизированные файлы содержат в своих именах расширение .min. Это можно заметить в папке Scripts проекта для файлов библиотек jQuery и jQuery UI, которые описаны в таблице ниже:

Библиотека JS Несжатый файл Минимизированный файл
jQuery jquery-2.1.3.js jquery-2.1.3.min.js
jQuery UI jquery-ui-1.11.2.js jquery-ui-1.11.2.min.js

Эффект от минимизации может оказаться существенным. Например, несжатый файл библиотеки jQuery имеет размер 261 Кбайт, тогда как минимизированный файл -всего 92 Кбайт. Такая разница может показаться не особо существенным достижением, но в случае доставки одного и того же файла JavaScript тысячам клиентов экономия полосы пропускания становится значительной. Это не только помогает снизить стоимость хостинга приложения, но также позволяет гораздо быстрее загрузить файл JavaScript в браузер и отобразить приложение пользователю (как известно, пользователи довольно-таки нетерпеливы).

Это представляет собой проблему управления сценариями, поскольку во время разработки мы хотим пользоваться несжатыми версиями файлов JavaScript. Мы также хотим изменять атрибуты src элементов <script> с целью применения минимизированных версий для финального тестирования и развертывания - такое действие обычно должно предприниматься везде, где используются файлы JavaScript. Разумеется, это также длительная и подверженная ошибкам задача.

Использование Bundles-упаковки

Средство упаковки появилось в версии ASP.NET 4.5, и оно помогает справиться с проблемами, которые были описаны в предыдущем разделе. Пакет - это набор файлов, которые трактуются как единое целое, с возможностями автоматической обработки версий файлов и переключения между несжатыми и минимизированными вариантами файлов. Здесь мы покажем, как установить и сконфигурировать пакеты, и объясним, каким образом они работают.

Подготовка проекта для пакетов

Пакеты конфигурируются при первом запуске приложения, поэтому мы следуем соглашению, которое было описано для маршрутизации URL. Для начала мы добавляем в проект папку App_Start и создаем в ней файл класса по имени BundleConfig.cs. Содержимое этого файла приведено ниже:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            // Здесь будут определяться пакеты
        }
    }
}

Пространство имен System.Web.Optimization содержит классы, поддерживающие средство упаковки. Вскоре мы будем применять объект BundleCollection, передаваемый методу RegisterBundles(), для установки пакета. Однако первым делом понадобится добавить глобальный класс приложения, в котором при запуске приложения будет вызываться метод BundleConfig.RegisterBundles(), как показано в примере ниже:

using System;
using System.Web.Optimization;

namespace ClientDev
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }
}

Методу RegisterBundles() передается объект BundleCollection, возвращаемый статическим свойством BundleTable.Bundles. Вынесение конфигурации в собственный класс позволяет тестировать код регистрации пакетов независимо от остальных частей приложения.

Создание пакета сценариев

После завершения подготовки пакет определяется довольно просто. В примере ниже приведено модифицированное содержимое файла класса App_Start\BundleConfig.cs с определением пакетов, требуемых в примере приложения:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            Bundle jquery = new ScriptBundle("~/bundle/jquery")
                .Include("~/Scripts/jquery-{version}.js");
            Bundle jqueryui = new ScriptBundle("~/bundle/jqueryui")
                .Include("~/Scripts/jquery-{version}.js", "~/Scripts/jquery-ui-{version}.js");

            bundles.Add(jquery);
            bundles.Add(jqueryui);
        }
    }
}

Здесь определены два пакета сценариев, названные jquery и jqueryui. Мы создаем пакет за счет создания нового экземпляра класса ScriptBundle с передачей его конструктору значения URL, представленного относительно корня приложения, под которым пакет будет включен внутрь веб-формы. По соглашению URL предваряется префиксом ~/bundle, так что никаких конфликтов между пакетами и ресурсами других типов, содержащимися в приложении, возникать не будет.

Включаемые в пакет файлы JavaScript указываются с применением метода Include(), который принимает один или большее число имен файлов, указанных в виде URL относительно корня приложения. Пакет jquery содержит файл сценариев jQuery, а пакет jqueryui - файлы сценариев библиотек jQuery и jQuery UI.

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

Bundle jquery = new ScriptBundle("~/bundle/jquery")
    .Include("~/Scripts/jquery-{version}.js");

Конструкция {version} очень удобна, т.к. она соответствует любой версии указанного файла, которой в данном случае является jquery-2.1.3.js, но будет также соответствовать любой другой версии библиотеки jQuery, установленной в папке Scripts. Это великолепно сочетается с применением диспетчера пакетов NuGet, поскольку позволяет обновлять используемые библиотеки и включать содержащиеся в них файлы в создаваемые пакеты.

Применение пакета сценариев

После того как пакет сценариев определен, его можно применить к веб-формам, которым требуются содержащиеся в нем файлы, и удалить явные элементы <script>, используемые ранее. Увидеть, как это делается, можно в примере ниже, в котором демонстрируется применение одного из пакетов, определенных в предыдущем разделе, к элементу <head> веб-формы Default.aspx:

...
<head runat="server">
    <title></title>
    <link rel="stylesheet" href="MainStyles.css" />
    <link rel="stylesheet" href="ErrorStyles.css" />
    <link rel="stylesheet" href="Content/themes/base/all.css" />
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jqueryui") %>
    <script>
        $(document).ready(function () {
            $('input[type=submit]').button();
        });
    </script>
</head>
...

Мы добавили фрагмент кода, в котором вызывается метод Scripts.Render() с передачей ему URL, определенного для требуемого пакета сценариев - в этом случае ~/bundle/jqueryui.

Каждый пакет является самодостаточным, поэтому нет необходимости в добавлении пакета jquery, т.к. файл библиотеки jQuery также включен в пакет jqueryui.

Чтобы увидеть результат использования пакета, запустите приложение и просмотрите HTML-разметку, отображаемую браузером для веб-формы Default.aspx. Раздел head будет содержать набор элементов <script>, ссылающихся на файлы JavaScript, которые были добавлены в пакет:

...
<script src="/Scripts/jquery-2.1.3.js"></script>
<script src="/Scripts/jquery-ui-1.11.2.js"></script>
...

Явные элементы <script> заменены единственным пакетом, который автоматически включает последние версии указанных файлов JavaScript. Это означает, что нам больше не придется модифицировать ссылки в отдельных файлах веб-форм при обновлении пакета jQuery.

Избегайте дублирования файлов в пакетах

В случае применения множества пакетов к веб-форме может возникнуть ситуация, когда на один и тот же файл JavaScript будут существовать ссылки сразу в нескольких пакетах. Распространенная ошибка связана с использованием для импортирования пакетов множества фрагментов кода, что приводит дублированию общего файла. Чтобы продемонстрировать проблему, мы добавили к веб-форме Default.aspx второй пакет:

...
<head runat="server">
    ...
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jqueryui") %>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery") %>
    ...
</head>
...

Мы добавили ранее определенный пакет jquery. В результате оба пакета, примененные к веб-форме Default.aspx, содержат файл /Scripts/jquery-2.1.3.js. Это приводит к возникновению проблемы, которую можно увидеть, запустив приложение и запросив упомянутую веб-форму. В HTML-разметке, отправляемой браузеру, мы создали два элемента <script> для одного и того же файла:

...
<script src="/Scripts/jquery-2.1.3.js"></script>
<script src="/Scripts/jquery-ui-1.11.2.js"></script>
<script src="/Scripts/jquery-2.1.3.js"></script>
...

Большинство браузеров достаточно интеллектуальны, чтобы не запрашивать один и тот же файл дважды, однако проблема, вызываемая вторым элементом <script>, заключается в том, что определяемые библиотекой jQuery глобальные переменные и функции после загрузки пакета jQuery UI инициализируются заново. Не вдаваясь в детали функционирования библиотек JavaScript, отметим, что второй элемент <script> нарушит работу jQuery UI и приведет к генерации ошибки (или, в зависимости от браузера, просто к отказу библиотеки jQuery UI безо всяких сообщений и отсутствию трансформации элементов <input> в кнопки с помощью виджета Button).

Корректный способ добавления множества пакетов к веб-форме предусматривает передачу URL пакетов в единственный вызов метода Render(), как показано в примере ниже:

...
<head runat="server">
    ...
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery", "~/bundle/jqueryui") %>
    ...
</head>
...

Метод Render() принимает множество URL пакетов и объединяет набор файлов, на которые ссылаются пакеты, обеспечивая отсутствие дублирования. Обнаружение и удаление дублированных файлов не работает при включенных оптимизациях пакетов. Оптимизации пакетов рассматриваются в следующей статье.

Создание пакета стилей CSS

Пакеты также можно использовать для таблиц стилей CSS. Работа с таблицами стилей не так сложна и проблематична, как работа с файлами JavaScript. Плавная задача при этом - обеспечить применение к веб-форме правильных таблиц стилей. По мере того, как используемые в проекте стили становятся более сложными и распространяются на все большее число файлов, работа по поддержанию элементов <link> внутри веб-форм в актуальном состоянии становится обременительной - к тому же очень легко пропустить какой-то файл или допустить опечатку в его имени.

В примере ниже демонстрируется добавление пакета стилей в приложение:

using System;
using System.Web.Optimization;
using System.Web.UI;

namespace ClientDev
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            Bundle jquery = new ScriptBundle("~/bundle/jquery")
                .Include("~/Scripts/jquery-{version}.js");
            Bundle jqueryui = new ScriptBundle("~/bundle/jqueryui")
                .Include("~/Scripts/jquery-{version}.js", "~/Scripts/jquery-ui-{version}.js");

            Bundle basicStyles = new StyleBundle("~/bundle/basicCSS")
                    .Include("~/MainStyles.css", "~/ErrorStyles.css");
            Bundle jqueryUIStyles = new StyleBundle("~/bundle/jqueryUICSS")
                    .IncludeDirectory("~/Content/themes/base", "*.css");

            bundles.Add(jquery);
            bundles.Add(jqueryui);
            bundles.Add(basicStyles);
            bundles.Add(jqueryUIStyles);
        }
    }
}

Мы определили два пакета стилей. Первый содержит таблицы стилей, созданные в начале статьи, а второй - файлы CSS, которые поступают вместе с пакетами jQuery UI. С помощью метода IncludeDirectory() в пакет включаются все файлы из папки Content\themes\base. Метод IncludeDirectory() доступен для пакетов сценариев и пакетов стилей. В качестве аргументов он принимает имя папки и шаблон поиска для файлов, которые должны быть включены в пакет. (На самом деле для работы библиотеки jQuery UI не нужны все файлы в этой папке, но мы включаем их просто для того, чтобы продемонстрировать применение пакетов.)

В примере ниже оба пакета стилей применяются в элементе <head> веб-формы Default.aspx:

...
<head runat="server">
    <title></title>
    <%: System.Web.Optimization.Styles.Render("~/bundle/basicCSS", 
        "~/bundle/jqueryUICSS") %>
    <%: System.Web.Optimization.Scripts.Render("~/bundle/jquery", "~/bundle/jqueryui") %>
    <script>
        $(document).ready(function () {
            $('input[type=submit]').button();
        });
    </script>
</head>
...

Мы следуем такому же подходу, как и при добавлении пакетов сценариев, но только здесь вызываем метод Styles.Render().

Система упаковки не проводит различия между файлами, находящимися внутри пакетов, так что пакеты сценариев и пакеты стилей отличаются только HTML-элементами, которые они генерируют. В результате при добавлении пакета к веб-форме очень легко вызвать неподходящий метод. Среда ASP.NET не сообщит об ошибке, но некоторые браузеры сделают это, а веб-форма не будет функционировать должным образом.

Пройди тесты
Лучший чат для C# программистов