Недавно сложившиеся ситуации подтолкнули меня на поиски простого и небольшого, по размерам, загрузчика ресурсов. Но все мои поиски приводили к require.js, который меня по некоторым причинам не устраивает (это тема для отдельной статьи).
Поэтому было принято решение написать свой велосипед и заодно попрактиковаться.
В итоге был реализован компонент, который занимает 6.28 Кб в uglify‘цированном виде и 1.3 Кб в GZip.
Его ключевые «фишки»:
- Может загружать как *.js, так и *.css.
- Реализована пакетная система. У каждого пакета может быть отдельная конфигурация.
- Загрузка происходит пакетами. То есть достаточно вызвать метод load() у нужного пакета и он загрузит все файлы, которые в нем находятся.
- Может загружать как асинхронно, так и в режиме Lazy Loading (загрузка пакета осуществляется только после загрузки всей страницы).
- Есть встроенный менеджер пакетов, который упрощает базовые операции с пакетами. А именно: хранение, создание, удаление, загрузка.
- Реализованы namespace’ы (на самом деле, реализация очень простая и для небольших проектов это плюс).
Вот, собственно, описание его главных особенностей.
Под катом небольшой курс использования AJL и описание разработки некоторых составляющих.
Подключаем AJL
Все что нужно указать при подключении AJL — это атрибут data-ep. Вот пример (подключаем AJL в базовом layout):
//baseLayout.html <script src="js/vendor/AJL.min.js" data-ep="EntryPoint.js"></script>
После загрузки AJL, он загружает EntryPoint.js, в котором вы настраиваете пакеты.
Можно и не прописывать data-ep. Вы сами вправе решать, где вам удобнее настраивать AJL.
EntryPoint.js — это по сути скрипт, который при загрузке также выполняется.
Задаем пакеты
Рассмотрим пример подключения jQuery, его плагинов и ваших скриптов. В данном случае, был использован data-ep атрибут.
//EntryPoint.js AJL({ name: "jQuery", //имя пакета assets: ['js/vendor/jquery.min.js'], //массив с URL'ами к нужным ресурсам config: { //объект конфигурации async: false //запретить асинхронную загрузку } }, { name: "jQuery Plugins", assets: ['js/vendor/jquery.plugin.js', 'js/vendor/jquery.plugin2.js'], config: { depend: ['jQuery'] //указываем имя пакета, который нужно загрузить перед загрузкой этого } }, { name: "My Scripts", assets: ['js/foo.js', 'js/bar.js'], config: { lazy: true //загружаем этот пакет только при событии window.onload } }).loadAll(); //после создания пакетов, нам возвращается PackageManager. Благодаря chainloading мы можем сразу загрузить все пакеты
Выглядит EntryPoint достаточно чисто. В name указываем имя пакета. В assets — массив с URL’ами asset’ов. В config — объект с параметрами. Всего можно передать шесть параметров в config:
- async (bool) — асинхронно ли загружать
- lazy (bool) — дожидаться загрузки window
- depend (array) — зависимости
- scriptTypeAttr (string) — этот параметр выводиться в script type=""
- linkCssTypeAttr (string) — этот в link type=""
- linkCssRelAttr (string) — в link rel=""
Честно говоря, я так и не понял, зачем я вынес атрибуты для тегов в объект с параметрами.
Что же происходит в EntryPoint.js?
1. Создается три пакета с именами jQuery, jQuery Plugins, My Scripts. После успешного создания AJL вернет PackageManager, в котором существует метод loadAll(). Данный метод загружает пакеты. Загрузка всех пакетов происходит перебором массива и вызовом load(). Немаловажный фактор, который может повлиять на загрузку, загрузка происходит в порядке очереди создания пакетов. Поэтому лучше указать сначала «серьезные» библиотеки, а лишь затем — все остальное.
2. Первым загрузится jQuery не в асинхронном режиме. После начнется загрузка jQuery Plugins, но только после загрузки jQuery. И напоследок — My Scripts — после загрузки всех ресурсов на странице (Lazy Loading).
Таким образом, можно делать разные конфигурации с пакетами. Допустим, у нас есть две категорически разные страницы. На одной — панель управления для пользователя, а на второй — крутой редактор с пачками скриптов. Скрипты и стили, которые нужно грузить на этих страницах, полностью отличаются друг от друга. А скриптов-то много. Не грузить же все в кучу, что нужно и не нужно. Создавать два разных базовых layout’а? Зачем?
С AJL, решение данной ситуации можно предложить подобным образом.
У нас есть EntryPoint, в котором создаем все необходимые пакеты:
AJL({ name: "jQuery", assets: ['js/vendor/jquery.min.js'], config: { async: false } }, { name: "jQuery Plugins", assets: ['js/vendor/jquery.plugin.js', 'js/vendor/jquery.plugin2.js'], config: { depend: ['jQuery'] } }, { name: "Editor Scripts And Styles", assets: ['js/editor/foo.js', 'js/editor/bar.js', 'css/editor/style.css'], config: { depend: ['jQuery Plugins'] } }, { name: "My Dashboard Scripts", assets: ['js/foo.js', 'js/bar.js'], config: { lazy: true } });
Обратите внимание на loadAll(). Его здесь нет. Так как мы хотим полностью разграничить загрузку пакетов, то мы не вызываем loadAll(). Мы просто создаем их. А так как у нас есть views для редактора и панели статистики, в них мы можем вызвать загрузку нужного пакета вручную.
//dashboard.html <script>AJL("My Dashboard Scripts").load();</script> //editor.html <script>AJL("Editor Scripts And Styles").load();</script>
Обратите внимание на то, что мы загружаем только пакет с названием Editor Scripts And Styles. Так как разрешение зависимостей здесь сделано рекурсивным методом, то мы можем вызвать последнее звено и все. А оно уже, в свою очередь, загрузит jQuery Plugins, а там и jQuery.
Таким образом, можно выстраивать цепочки пакетов и загружать только те, которые действительно необходимы для работы.
Что происходит под «капотом»?
А теперь перейдем к тому, как разрабатывались некоторые модули AJL.
Namespace
Самым интересным, я считаю, реализацию namespace’ов в 17 строк кода. Здесь все просто и суть заключается в разбиении namespace’а на элементы массива и его итерацию. Когда доходим до последнего элемента, то на этот элемент назначаем модуль.
Привожу код функции, которая вызывается при создании namespace’а.
setNamespace: function (namespace, module) { var parts = namespace.split('.'), parent = window, partsLength, curPart, i; //Need iterate all parts of namespace without last one partsLength = parts.length - 1; for (i = 0; i < partsLength; i++) { //Remember current part curPart = parts[i]; if (typeof parent[curPart] === 'undefined') { //If this part undefined then create empty parent[curPart] = {}; } //Remember created part in parent parent = parent[curPart]; } //And last one of parts need to be filled by module param parent[parts[partsLength]] = module; //And not forgot return generated namespace to global scope return parent; },
В итоге, при разработке своих модулей, можно использовать довольно простую конструкцию:
AJL("Module.SubModule", function() { return "Hi, I'm Module.SubModule"; });
Package, PackageConfig, Loader
Пакеты и конфигурация пакетов являются лишь функциями с прототипом (классом, одним словом). Все что я храню в их свойствах — это имя пакета, массив URL’ов, instance конфигурации пакета. Сам метод load() у Package вызывает статическую функцию loadPackage() из Loader.js с применением call().
load: function () { AJL.Loader.loadPackage.call(this); }
Это сделано для того, чтобы уберечь себя от нехорошего дублирования кода. Пакеты разные, конфигурации разные, а загрузчик-то один должен быть. Вот собственно Loader.js и loadPackage() принимают решения, когда можно добавить в DOM тэг, а когда нет.
loadPackage: function () { var helper = AJL.Helper, packageManager = AJL.PackageManager, pack = this, packageAssets = pack.getAssets(), packageConfig = pack.getConfig(), depend = packageConfig.getItem('depend'); //If assets array empty then halt loading of package if (helper.isEmpty(packageAssets)) { return false; } //If this package depend on other packages then load dependencies first if (!helper.isEmpty(depend)) { packageManager.loadByNames(depend); } //If need to wait window.load than call lazyLoad and return if (packageConfig.getItem('lazy') == true) { lazyLoad.call(pack); return true; } //In other cases just call startLoading directly for start loading startLoading.call(pack); return true; },
Обращаем внимание на то, как реализована загрузка зависимостей. Если в данном пакете есть зависимости, то мы вызываем рекурсивную загрузку. Грузим зависимости, и так далее, по цепочке вверх.
PackageManager
Также немаловажной частью AJL является PackageManager, который управляет пакетами. Такой себе коллектор. Есть getters, есть setters, которые проверяют запрошенное имя в массиве instance’ов пакетов, а также, является ли объект instance’ом Package’а. Если да, то возвращаем его, либо производим нужные нам действия с ним. К примеру, рассмотренная функция loadAll() действует обычным перебором.
loadAll: function () { var helper = AJL.Helper, curPack; for (var pack in packages) { if (packages.hasOwnProperty(pack)) { curPack = packages[pack]; if (helper.isInstanceOf(curPack, AJL.Package)) { curPack.load(); } } } return this; },
Происходит перебор в массиве, и если это instanse Package, то вызываем load(). При загрузке зависимых пакетов я использую функцию loadByNames().
loadByNames: function (names) { var helper = AJL.Helper, curName, namesLength, i; namesLength = names.length; for (i = 0; i < namesLength; i++) { curName = names[i]; if (packages.hasOwnProperty(curName) && helper.isInstanceOf(packages[curName], AJL.Package)) { packages[curName].load(); } } return this; }
Перебираем весь массив с именами и смотрим, есть ли эти имена в нашем storage пакетов. Если да и он instance Package’а, то вызываем load().
AJL
И напоследок самое главное. Функция AJL().
AJL = function () { var packageManager = AJL.PackageManager, namespace = AJL.Namespace, helper = AJL.Helper, packageInstance = {}, packageName = '', packageAssets = [], packageConfig = {}, argLength = arguments.length, argFirst, argSecond, i; //Switch of arguments length for detect what need to do switch (argLength) { case 0: //If arguments not exists then just return PackageManager instance return packageManager; case 1: argFirst = arguments[0]; //If this arg is string then return package with this name if (helper.isString(argFirst)) { return packageManager.getPackage(argFirst); } break; case 2: argFirst = arguments[0]; argSecond = arguments[1]; //If first arg is string and second object or function if (helper.isString(argFirst) && (helper.isObject(argSecond) || helper.isFunction(argSecond))) { //Then I think that it's namespace setting namespace.setNamespace(argFirst, argSecond); return packageManager; } break; default: break; } //If all predefined templates in arguments didn't decided then create packages from them for (i = 0; i < argLength; i++) { if (!helper.isUndefined(arguments[i])) { packageName = arguments[i].name; packageAssets = arguments[i].assets; packageConfig = arguments[i].config; packageInstance = new AJL.Package(packageName, packageAssets, packageConfig); packageManager.setPackage(packageInstance); } } return packageManager; };
Сначала смотрим на количество аргументов, переданных в функцию. Если ничего не передавали, то сразу возвращает PackageManager. Если же передали одну строку, то предполагаем, что нам дали имя пакета и ищем его. После находки возвращаем объект Package. Если передано было два параметра и первый из них строка, то кидаем второй параметр в этот namespace (из первого параметра). И наконец, если ни одно не подошло, то считаем, что это стартовая конфигурация для AJL и создаем все Package в нем.
Благодарю всех, кто нашел силы дочитать до этого места. Если хотите попробовать AJL, то есть все необходимые ресурсы:
Главная страница
Исходники на GitHub
Документация
P.S. Буду признателен за все пожелания и критику. Бросать разработку не собираюсь. Просто закончились идеи 🙂
ссылка на оригинал статьи http://habrahabr.ru/post/202450/
Добавить комментарий