[Из песочницы] Минификация приложений ExtJS и Sencha Touch средствами ASP.NET

Если вы пишете веб-приложения на ExtJS в связке с ASP.NET MVC и хотите минифицировать исходные файлы, но по каким-то причинам вам не нравится использовать для этого стандартный SenchaCmd, добро пожаловать под кат. Для тех, у кого нет времени и уже хочется попробовать, в конце статьи есть ссылки на библиотеку, а пока попробуем разобраться, в чём проблема и написать такой минификатор самостоятельно.Что будет в итоге public class BundleConfig { public static void RegisterBundles (BundleCollection bundles) { bundles.Add ( new SenchaBundle (»~/bundles/my-sencha-app») .IncludeDirectory (»~/Scripts/my-sencha-app»,»*.js», true) ); } } Intro Итак, вы разрабатываете с помощью библиотек ExtJS 4 или SenchaTouch 2, и ваши веб-приложения структурированы так, как это рекомендуют сами разработчики библиотеки. С ростом приложения количество исходников увеличивается, что наверняка приводит к задержке загрузки, ну или вы просто хотите скрыть свой красивый исходный код от чужих глаз.Первое, что приходит в голову это использовать SenchaCmd — продукт, который рекомендует команда Sencha. Ему можно скормить файл index.html или URL приложения, он послушно возьмёт страницу и отследит, в каком порядке были загружены исходники, после чего отдаст минификатору, и на выходе вы получите что хотели.

В чём неудобство? Здесь мнения могут разниться, но IMHO для сжатия файлов SenchaCmd тяжеловат. В процессе участвуют Java-приложение, nodejs и phantomjs. В принципе, для таких редких операций как минификация перед загрузкой на сервер, может и сгодится, но есть ещё нюансы. Например, Index.cshtml ему не отдашь: участки с Razor-разметкой не поймёт. Можно дать URL приложения, но если у вас используется аутентификация до прохождения которой загружается не всё приложение, то в минифицированном файле тоже будут не все исходники. А в случае с Windows-аутентификацией вообще всё плохо.

Намного проще было бы сказать: «Вот тебе папка, сам разберись, что к чему и дай мне сжатый файл». На просторах интернета полным-полно минификаторов, но среди нет тех, кто мог бы установить зависимости между исходными файлами. Попробуем это исправить.

Приступим В стеке ASP.NET уже есть инструмент для конкатенации и минификации — Bundles. Ему нужно только немного помочь —, а именно, подсказать, в каком порядке склеивать исходники.BundleConfig.cs public class BundleConfig { public static void RegisterBundles (BundleCollection bundles) { bundles.Add ( new ScriptBundle (»~/bundles/my-sencha-app») { Orderer = // ? } .IncludeDirectory (»~/Scripts/my-sencha-app»,»*.js», true); ); } } То, что нужно! Посмотрим на Orderer.IBundleOrderer public interface IBundleOrderer { IEnumerable OrderFiles (BundleContext context, IEnumerable files); } На входе коллекция файлов, на выходе — тоже, только отсортированная. Давайте подумаем, как их упорядочить. В ExtJS есть несколько способов определить зависимости.Явные:

Вручную в коде через Ext.require Через конфигурационное свойство класса requires Неявные (только конфигурационные свойства): При наследовании — extend При указании примесей — mixins При указании модели хранилища — model При указании представлений, моделей и хранилищ контроллера — views, models, stores При указании контроллеров приложения — controllers При автоматическом создании Viewport — autoCreateViewport: true К первому случаю претензий иметь не будем — в коде значит в коде. Остальные вполне поддаются анализу.Определимся со структурой программы. Для начала у нас есть JS-файл. Он может иметь несколько классов внутри, каждый из которых может иметь зависимости на другие классы:

SenchaFile.cs public class SenchaFile { ///

/// Классы внутри файла /// public IEnumerable Classes { get; set; }

///

/// Зависимости файла /// public virtual IEnumerable Dependencies { get; set; } } SenchaClass.cs public class SenchaClass { /// /// Имя класса /// public string ClassName { get; set; }

///

/// Имена зависимостей /// public IEnumerable DependencyClassNames { get; set; } } Теперь нужно как-то определить, какие классы описаны в файлах. Можно поискать регулярками, например, но я бы отложил этот скилл на потом. Тем более, что у нас есть JSParser из Microsoft.Ajax.Utilities. Он выдаёт содержимое JS-файла в виде дерева блоков, каждый из которых может быть например, вызовом функции, обращению к свойству и т.д. Поищем, где в файле создаются экземпляры приложения (Ext.application), определяются или переопределяются классы (Ext.define, Ext.override): SenchaFile.cs public class SenchaFile { // … /// /// Получить классы, описанные в файле /// protected virtual IEnumerable GetClasses () { var extApps = this.RootBlock.OfType() .Where (cn => cn.Children.Any ()) .Where (cn => cn.Children.First ().Context.Code == «Ext.application») .Select (cn => cn.Arguments.OfType().First ()) .Select (arg => new SenchaClass (arg) { IsApplication = true });

var extDefines = this.RootBlock.OfType() .Where (cn => cn.Arguments.OfType().Any ()) .Where (cn => cn.Arguments.OfType().Any ()) .Where (cn => { var code = cn.Children.First ().Context.Code; return code == «Ext.define» || code == «Ext.override»; }) .Select (cn => { var className = cn.Arguments.OfType().First ().Value.ToString (); var config = cn.Arguments.OfType().First (); return new SenchaClass (config) { ClassName = className }; });

foreach (var cls in extApps.Union (extDefines)) { yield return cls; } } } Следующим шагом необходимо определить зависимости каждого класса. Для этого возьмём тот же JSParser и пройдёмся по всем случаям определения зависимостей (явным и неявным), описанным выше. Приводить код не буду, чтобы не загружать статью, но суть та же: перебираем дерево блоков в поисках нужных свойств и выбираем имена используемых классов.Теперь у нас в наличии список файлов, у каждого файла найдены описанные в нём классы, а у каждого класса — его зависимости. И нужно как-то расставить их в порядке очереди. Для этого существует так называемая топологическая сортировка. Алгоритм несложный и для интересующихся есть онлайн-демка:

77f937b76f2b2cdbb0731ffe5bbc37bb.png

SenchaOrderer.cs public class SenchaOrderer { ///

/// Рекурсивная функция топологической сортировки /// /// Узел, с которого начинать /// Лист файлов в порядке очереди protected virtual void DependencyResolve(TNode node, IList resolved) where TNode: SenchaFile { // При входе в узел помечаем его серым node.Color = SenchaFile.SortColor.Gray;

// Идём по его зависимостям foreach (TNode dependency in node.Dependencies) { // Если мы в этом узле не были (он белый), заходим вглубь if (dependency.Color == SenchaFile.SortColor.White) { DependencyResolve (dependency, resolved); } // А если были (серый), то всё плохо: есть циклическая зависимость else if (dependency.Color == SenchaFile.SortColor.Gray) { throw new InvalidOperationException (String.Format ( «Circular dependency detected: '{0}' → '{1}'», node.FullName? String.Empty, dependency.FullName? String.Empty) ); } }

// Но лучше, чтобы циклов не было… // При выходе из узла добавляем его в очередь, метим чёрным и больше не возвращаемся. node.Color = SenchaFile.SortColor.Black; resolved.Add (node); }

///

/// Отсортировать файлы используя топологическую сортировку /// /// Файлы для сортировки /// Отсортированная коллекция SenchaFileInfo public virtual IEnumerable OrderFiles(IEnumerable files) where TNode: SenchaFile { var filelist = files.ToList ();

// Коллекции файлов с неразрешёнными и разрешёнными зависимостями IList unresolved = filelist; IList resolved = new List();

TNode startNode = unresolved .Where (ef => ef.Color == SenchaFile.SortColor.White) .FirstOrDefault ();

while (startNode!= null) { DependencyResolve (startNode, resolved); startNode = unresolved .Where (ef => ef.Color == SenchaFile.SortColor.White) .FirstOrDefault (); }

return resolved; } } Вот как бы и всё. Ещё пара служебных файлов и можно пользоваться:

BundleConfig.cs

public class BundleConfig { public static void RegisterBundles (BundleCollection bundles) { bundles.Add ( new SenchaBundle (»~/bundles/my-sencha-app») .IncludeDirectory (»~/Scripts/my-sencha-app»,»*.js», true) ); } } Index.cshtml

Итого В чём плюсы такого решения? Я думаю, очевидно: использовать стандартную функциональность, предусмотренную фреймворком ASP.NET. В чём минусы? Они тоже есть: Старт веб-приложения несколько задерживается, пока минифицируются файлы. Алгоритм чувствителен к написанию кода, например, autoCreateViewport: true он поймёт, а autoCreateViewport: !0 — уже нет (без допиливания). Приложение ExtJS или SenchaTouch необходимо создавать строго через вызов Ext.application. Такой минификатор используется у нас в нескольких проектах, один из которых имеют своеобразную структуру файлов. В основном, после его подключения, они завелись без проблем, но в том своебразном пришлось чуть-чуть подправить исходники, чтобы убрать спагетти зависимостей.Попробовать NuGet. Пакет SenchaMinify. Проект на GitHub с демками. На гитхабе также включён проект самостоятельного exe-файла для командной строки (SenchaMinify.Cmd). Так что желающие могут использовать свои любимые средства автоматизации.Буду рад конструктиву, идеям или пулл-реквестам.

© Habrahabr.ru