Pull to refresh

WebMarkupMin: Минимизация представлений KnockoutJS и AngularJS

Reading time8 min
Views7.2K
Логотипы WebMarkupMin, KnockoutJS и AngularJS
Начиная с версия 0.9.0 в WebMarkupMin поддерживается минимизация представлений KnockoutJS (далее просто Knockout) и AngularJS (далее просто Angular). Многие из вас могут задать вопрос: «Почему Knockout и Angular, а не Mustache или Underscore?». Этот выбор был сделан по следующим причинам:
  1. Шаблоны на основе DOM. Шаблонизаторы, встроенные в Knockout и Angular, базируются на DOM-шаблонах (DOM-based templates), а не на строковых шаблонах (string-based templates) как Mustache и Underscore. Код таких шаблонов не содержит программных вставок (например, {{…}} или <%…%>) за пределами текстового содержимого элементов (тегов) и значений атрибутов, что позволяет минимизировать его как обычный HTML.
  2. Популярность среди .NET-разработчиков. Knockout изначально создавался для .NET-разработчиков, чтобы позволить им перенести свой опыт разработки MVVM-приложений из WPF и Silverlight в обычный веб. Что же касается Angular, то он вообще не нуждается в представлении и его популярность среди веб-разработчиков в целом бьет все возможные рекорды. Помимо этого популярности этих библиотек среди .NET-разработчиков способствовало огромное количество статей евангелиста Microsoft Джона Папы.
  3. Высокая эффективность сжатия выражений привязки. Выражения привязки в Knockout и Angular фактически являются простым JavaScript-кодом или объектами в формате JSON, которые можно сжать JS-минимизатором.


Новые конфигурационные свойства


По умолчанию минимизация представлений отключена, и чтобы включить ее нужно изменить значения следующих свойств класса HtmlMinificationSettings:
Свойство Тип данных Значение по умолчанию Описание
ProcessableScriptTypeList Строка Пустая строка Содержит разделенный запятыми список типов тегов script (например, text/html,text/ng-template), содержимое которых должно быть обработано HTML-минимизатором.
MinifyKnockoutBindingExpressions Булевский false Флаг, отвечающий за минимизацию выражений привязки Knockout в атрибутах data-bind и безконтейнерных комментариях.
MinifyAngularBindingExpressions Булевский false Флаг, отвечающий за минимизацию выражений привязки Angular в Mustache-подобных тегах ({{ }}) и директивах.
CustomAngularDirectiveList Строка Пустая строка Содержит разделенный запятыми список пользовательских Angular-директив (например, myDir,btfCarousel), которые содержат выражения привязки. Если значение свойства MinifyAngularBindingExpressions равно true, то выражения в пользовательских директивах будут минимизированы.
Рассмотрим каждое свойство более подробно:

ProcessableScriptTypeList


До версии 0.9.0 теги script, не содержащие JavaScript-кода, просто игнорировались HTML-минимизатором. Это было сделано потому, что в этих тегах может содержаться все что угодно: от кода на VBScript до Handlebars-шаблонов. Но в тоже время, если тег script содержит код DOM-шаблона, то его стоило бы пропустить через HTML-минимизатор. Поэтому пользователям была дана возможность самим определять список допустимых типов содержимого.

Возвращаясь к теме статьи, приведу несколько примеров:
  1. Если нужно минимизировать представления Knockout, то свойству ProcessableScriptTypeList нужно присвоить значение равное text/html.
  2. Если мы имеем дело с представлениями Angular, то — text/ng-template.
  3. Если в проекте используются сразу две библиотеки, то нужно перечислить типы содержимого через запятую: text/html,text/ng-template.

Также нужно понимать, что мы не ограничены только Knockout и Angular, теоретически у нас есть возможность минимизировать любой основанный на DOM шаблон (например, мы можем указать тип содержимого text/x-kendo-tmpl для представлений Kendo MVVM, которые мало чем отличаются от Knockout).

MinifyKnockoutBindingExpressions


Обычно когда я рассказываю про WebMarkupMin, то в качестве примера привожу код файла shell.html из демонстрационного проекта HotTowel:

<div>
    <header>
        <!--ko compose: {view: 'nav'} --><!--/ko-->
    </header>
    <section id="content" class="main container-fluid">
        <!--ko compose: {model: router.activeItem,
            afterCompose: router.afterCompose,
            transition: 'entrance'} -->
        <!--/ko-->
    </section>
    <footer>
        <!--ko compose: {view: 'footer'} --><!--/ko-->
    </footer>
</div>

После минимизации этот код принимает следующий вид:

<div><header><!--ko compose: {view: 'nav'}--><!--/ko--></header><section id="content" class="main container-fluid"><!--ko compose: {model: router.activeItem,
            afterCompose: router.afterCompose,
            transition: 'entrance'}--> <!--/ko--></section><footer><!--ko compose: {view: 'footer'}--><!--/ko--></footer></div>

Сразу бросается в глаза, что выражения привязки (далее просто выражения) в безконтейнерных комментариях (так называемый containerless control flow syntax) содержат слишком много пробельных символов и их стоило бы минимизировать.

Возьмем для примера выражение compose: {view: 'nav'}, которое, по своей сути, является объектом в формате JSON без внешних фигурных скобок. Мы можем обернуть его в фигурные скобки: {compose: {view: 'nav'}}, и обработать JS-минимизатором. Затем удалить из минимизированного кода внешние фигурные скобки и вернуть его обратно в безконтейнерный комментарий.

Если свойству MinifyKnockoutBindingExpressions присвоить значение равное true, то к выражениям будут применены все описанные выше действия. Для этих целей в качестве JS-минимизатора всегда используется CrockfordJsMinifier, потому что только он может корректно минимизировать JSON-код (MsAjaxJsMinifier и YuiJsMinifier не подходят для этих целей).

С новыми настройками мы получим следующий код:

<div><header><!--ko compose:{view:'nav'}--><!--/ko--></header><section id="content" class="main container-fluid"><!--ko compose:{model:router.activeItem,afterCompose:router.afterCompose,transition:'entrance'}--> <!--/ko--></section><footer><!--ko compose:{view:'footer'}--><!--/ko--></footer></div>

Те же самые действия производятся и с атрибутами data-bind. Например, у нас есть следующий код:

<span data-bind="text: price() > 50 ? 'expensive' : 'cheap'"></span>

После минимизации он будет выглядеть следующим образом:

<span data-bind="text:price()>50?'expensive':'cheap'"></span>

MinifyAngularBindingExpressions


Согласно документации, выражения в Angular – это JavaScript-подобные фрагменты кода. Тем не менее, их можно минимизировать с помощью CrockfordJsMinifier. Единственную проблему представляли лишь одноразовые выражения привязки (начинаются с ::). Оригинальный JSMin всегда удалял идущие перед ними пробельные символы, но после внесения небольших изменений в код CrockfordJsMinifier проблема была устранена.

В Angular выражения могут содержаться в Mustache-подобных тегах ({{ }}) и директивах.

В свою очередь, Mustache-подобные теги могут находиться в текстовом содержимом элементов:

<strong>Price:</strong> {{ 3 * 10 | currency }}

и в значениях атрибутов:

<img src="/Content/images/icons/{{  iconName + '.png'  }}">

Соответственно, если свойству MinifyAngularBindingExpressions присвоить значение равное true, то мы получим следующие результаты:

<strong>Price:</strong> {{3*10|currency}}
и
<img src="/Content/images/icons/{{iconName+'.png'}}">

С директивами дело обстоит намного сложнее. В коде шаблонов директивы могут быть представлены 4-мя способами:
  1. В виде элементов
  2. В виде атрибутов
  3. Как содержимое атрибутов class
  4. В виде комментариев

Названия директив, также могут иметь различные варианты написания. Например, для директивы ngBind существуют следующие варианты:
  • ng-bind
  • ng:bind
  • ng_bind
  • x-ng-bind
  • data-ng-bind

Все эти особенности учитываются при минимизации.

Кроме того, не все директивы содержат выражения. Некоторые директивы могут также содержать: шаблоны, простые значения или вообще ничего. Для того, чтобы отделить директивы, содержащие выражения, от других директив используется следующий список: ngBind, ngBindHtml, ngBlur, ngChange, ngChecked, ngClass, ngClassEven, ngClassOdd, ngClick, ngController, ngCopy, ngCut, ngDblclick, ngDisabled, ngFocus, ngHide, ngIf, ngInit, ngKeydown, ngKeypress, ngKeyup, ngModelOptions, ngMousedown, ngMouseenter, ngMouseleave, ngMousemove, ngMouseover, ngMouseup, ngOpen, ngPaste, ngReadonly, ngRepeat, ngRepeatStart, ngSelected, ngShow, ngStyle, ngSubmit и ngSwitch.

Элементы


HTML-минимизатор обрабатывает всего одну директиву-элемент — ngPluralize. Если говорить более конкретно, то минимизируются выражения в атрибутах count и when.

Предположим, что у нас есть следующий код:

<ng-pluralize count="  personCount  "
    when="{ '0': 'Nobody is viewing.',
            'one': '1 person is viewing.',
            'other': '{} people are viewing.' }">
</ng-pluralize>

После минимизации он будет выглядеть следующим образом:

<ng-pluralize count="personCount" when="{'0':'Nobody is viewing.','one':'1 person is viewing.','other':'{} people are viewing.'}"> </ng-pluralize>

Атрибуты


Минимизация выражений в директивах-атрибутах в Angular даже проще, чем минимизация содержимого атрибутов data-bind в Knockout, потому что здесь не нужно думать о внешних фигурных скобках.

Рассмотрим минимизацию выражений в директивах-атрибутах на примере директивы ngRepeat:

<li ng-repeat="customer in customers | filter:nameText | orderBy:'name'">
	{{customer.name}} - {{customer.city}}
</li>

Несмотря на то, что атрибут содержит «некорректный» код (с точки зрения JavaScript), минимизация все равно проходит успешно:

<li ng-repeat="customer in customers|filter:nameText|orderBy:'name'">{{customer.name}} - {{customer.city}}</li>

Классы


Главное отличие директив-классов от директив-атрибутов заключается в том, что в атрибуте class может содержаться сразу несколько директив:

<span class="ng-bind: name; ng-cloak; ng-style: { 'background-color': 'lime' };"></span>

Была решена и эта проблема:

<span class="ng-bind:name;ng-cloak;ng-style:{'background-color':'lime'}"></span>

Комментарии


К сожалению, в Angular нет ни одной встроенной директивы, которая могла бы быть представлена в виде комментария. Поэтому мы рассмотрим немного надуманный пример, где в качестве директивы-комментария будет использоваться пользовательская директива myDir:

<!-- directive: my-dir 1 + 1 -->

После минимизации получим следующий код:

<!--directive:my-dir 1+1-->

CustomAngularDirectiveList


Поскольку в Angular существует возможность создавать свои собственные директивы, то пользователям была предоставлена возможность добавлять названия этих директив в специальный список. Пользовательские директивы, указанные в свойстве CustomAngularDirectiveList, будут обрабатываться минимизатором также как встроенные директивы Angular.

Поддержка в Web Essentials 2013


Если вы храните свои представления в отдельных HTML-файлах, то от стандартных ASP.NET-расширений, идущих в комплекте с WebMarkupMin, будет мало пользы. В этом случае вам нужен принципиально другой инструмент, который будет минимизировать HTML-файлы на этапе сборки проекта. На данный момент есть только один такой инструмент — это VS-расширение Web Essentials 2013.

Последняя стабильная версия Web Essentials 2013 (версия 2.3) поддерживает старую версию WebMarkupMin (версию 0.8.21). Поэтому вам нужно будет установить ночную сборку Web Essentials 2013 (на сайте расширения есть инструкция по установке).

Последняя, на момент написания статьи, ночная сборка (версия 2.3.4.1) поддерживает WebMarkupMin 0.9.3 и позволяет настроить все рассмотренные конфигурационные свойства:

Настройки HTML-минимизации в Web Essentials 2013 (версия 2.3.4.1)

Ссылки


  1. Страница проекта WebMarkupMin на CodePlex
  2. Статья «WebMarkupMin HTML Minifier – современный HTML-минимизатор для платформы .NET»
  3. Официальный сайт VS-расширения Web Essentials
  4. Статья «HTML-минимизация в Web Essentials 2013»
  5. Статья «HTML-минимизация в Web Essentials 2013: Что изменилось за год?»

UPD1: 17 октября вышла стабильная версия Web Essentials 2013 (версия 2.4), которая поддерживает весь представленный функционал.
Tags:
Hubs:
+10
Comments4

Articles

Change theme settings