- PVSM.RU - https://www.pvsm.ru -
Если вы пишете веб-приложения на ExtJS [1]в связке с 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)
);
}
}
Итак, вы разрабатываете с помощью библиотек ExtJS 4 или SenchaTouch 2, и ваши веб-приложения структурированы так, как это рекомендуют сами разработчики библиотеки. С ростом приложения количество исходников увеличивается, что наверняка приводит к задержке загрузки, ну или вы просто хотите скрыть свой красивый исходный код от чужих глаз.
Первое, что приходит в голову это использовать SenchaCmd [2] — продукт, который рекомендует команда Sencha. Ему можно скормить файл index.html или URL приложения, он послушно возьмёт страницу и отследит, в каком порядке были загружены исходники, после чего отдаст минификатору, и на выходе вы получите что хотели.
В чём неудобство? Здесь мнения могут разниться, но IMHO для сжатия файлов SenchaCmd тяжеловат. В процессе участвуют Java-приложение, nodejs и phantomjs. В принципе, для таких редких операций как минификация перед загрузкой на сервер, может и сгодится, но есть ещё нюансы. Например, Index.cshtml ему не отдашь: участки с Razor-разметкой не поймёт. Можно дать URL приложения, но если у вас используется аутентификация до прохождения которой загружается не всё приложение, то в минифицированном файле тоже будут не все исходники. А в случае с Windows-аутентификацией вообще всё плохо [3].
Намного проще было бы сказать: «Вот тебе папка, сам разберись, что к чему и дай мне сжатый файл». На просторах интернета полным-полно минификаторов, но среди нет тех, кто мог бы установить зависимости между исходными файлами. Попробуем это исправить.
В стеке ASP.NET уже есть инструмент для конкатенации и минификации — Bundles [4]. Ему нужно только немного помочь — а именно, подсказать, в каком порядке склеивать исходники.
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.
public interface IBundleOrderer
{
IEnumerable<BundleFile> OrderFiles(BundleContext context, IEnumerable<BundleFile> files);
}
На входе коллекция файлов, на выходе — тоже, только отсортированная. Давайте подумаем, как их упорядочить. В ExtJS есть несколько способов определить зависимости.
Явные:
Неявные (только конфигурационные свойства):
К первому случаю претензий иметь не будем — в коде значит в коде. Остальные вполне поддаются анализу.
Определимся со структурой программы. Для начала у нас есть JS-файл. Он может иметь несколько классов внутри, каждый из которых может иметь зависимости на другие классы:
public class SenchaFile
{
/// <summary>
/// Классы внутри файла
/// </summary>
public IEnumerable<SenchaClass> Classes { get; set; }
/// <summary>
/// Зависимости файла
/// </summary>
public virtual IEnumerable<SenchaFile> Dependencies { get; set; }
}
public class SenchaClass
{
/// <summary>
/// Имя класса
/// </summary>
public string ClassName { get; set; }
/// <summary>
/// Имена зависимостей
/// </summary>
public IEnumerable<string> DependencyClassNames { get; set; }
}
Теперь нужно как-то определить, какие классы описаны в файлах. Можно поискать регулярками, например, но я бы отложил этот скилл на потом. Тем более, что у нас есть JSParser из Microsoft.Ajax.Utilities. Он выдаёт содержимое JS-файла в виде дерева блоков, каждый из которых может быть например, вызовом функции, обращению к свойству и т.д. Поищем, где в файле создаются экземпляры приложения (Ext.application), определяются или переопределяются классы (Ext.define, Ext.override):
public class SenchaFile
{
// ..
/// <summary>
/// Получить классы, описанные в файле
/// </summary>
protected virtual IEnumerable<SenchaClass> GetClasses()
{
var extApps = this.RootBlock.OfType<CallNode>()
.Where(cn => cn.Children.Any())
.Where(cn => cn.Children.First().Context.Code == "Ext.application")
.Select(cn => cn.Arguments.OfType<ObjectLiteral>().First())
.Select(arg => new SenchaClass(arg) { IsApplication = true });
var extDefines = this.RootBlock.OfType<CallNode>()
.Where(cn => cn.Arguments.OfType<ConstantWrapper>().Any())
.Where(cn => cn.Arguments.OfType<ObjectLiteral>().Any())
.Where(cn =>
{
var code = cn.Children.First().Context.Code;
return code == "Ext.define" || code == "Ext.override";
})
.Select(cn =>
{
var className = cn.Arguments.OfType<ConstantWrapper>().First().Value.ToString();
var config = cn.Arguments.OfType<ObjectLiteral>().First();
return new SenchaClass(config) { ClassName = className };
});
foreach (var cls in extApps.Union(extDefines))
{
yield return cls;
}
}
}
Следующим шагом необходимо определить зависимости каждого класса. Для этого возьмём тот же JSParser и пройдёмся по всем случаям определения зависимостей (явным и неявным), описанным выше. Приводить код не буду, чтобы не загружать статью, но суть та же: перебираем дерево блоков в поисках нужных свойств и выбираем имена используемых классов.
Теперь у нас в наличии список файлов, у каждого файла найдены описанные в нём классы, а у каждого класса — его зависимости. И нужно как-то расставить их в порядке очереди. Для этого существует так называемая топологическая сортировка [5]. Алгоритм несложный и для интересующихся есть онлайн-демка [6]:
public class SenchaOrderer
{
/// <summary>
/// Рекурсивная функция топологической сортировки
/// </summary>
/// <param name="node">Узел, с которого начинать</param>
/// <param name="resolved">Лист файлов в порядке очереди</param>
protected virtual void DependencyResolve<TNode>(TNode node, IList<TNode> 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);
}
/// <summary>
/// Отсортировать файлы используя топологическую сортировку
/// </summary>
/// <param name="files">Файлы для сортировки</param>
/// <returns>Отсортированная коллекция SenchaFileInfo</returns>
public virtual IEnumerable<TNode> OrderFiles<TNode>(IEnumerable<TNode> files)
where TNode: SenchaFile
{
var filelist = files.ToList();
// Коллекции файлов с неразрешёнными и разрешёнными зависимостями
IList<TNode> unresolved = filelist;
IList<TNode> resolved = new List<TNode>();
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
...
<script src="@Url.Content("~/bundles/my-sencha-app")" type="text/javascript"></script>
В чём плюсы такого решения? Я думаю, очевидно: использовать стандартную функциональность, предусмотренную фреймворком ASP.NET. В чём минусы? Они тоже есть:
Такой минификатор используется у нас в нескольких проектах, один из которых имеют своеобразную структуру файлов. В основном, после его подключения, они завелись без проблем, но в том своебразном пришлось чуть-чуть подправить исходники, чтобы убрать спагетти зависимостей.
На гитхабе также включён проект самостоятельного exe-файла для командной строки (SenchaMinify.Cmd). Так что желающие могут использовать свои любимые средства автоматизации.
Буду рад конструктиву, идеям или пулл-реквестам.
Автор: alexstz
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/biblioteka-extjs-sencha/60765
Ссылки в тексте:
[1] ExtJS : http://www.sencha.com/
[2] SenchaCmd: http://www.sencha.com/products/sencha-cmd
[3] плохо: https://github.com/ariya/phantomjs/issues/11037
[4] Bundles: http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification
[5] топологическая сортировка: http://ru.wikipedia.org/wiki/%D0%A2%D0%BE%D0%BF%D0%BE%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0
[6] онлайн-демка: https://www.cs.usfca.edu/~galles/visualization/TopoSortDFS.html
[7] NuGet. Пакет SenchaMinify: https://www.nuget.org/packages/SenchaMinify/
[8] Проект на GitHub: https://github.com/alexeysolonets/SenchaMinify
[9] Источник: http://habrahabr.ru/post/224191/
Нажмите здесь для печати.