Недавно сложившиеся ситуации подтолкнули меня на поиски простого и небольшого, по размерам, загрузчика ресурсов. Но все мои поиски приводили к 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. Буду признателен за все пожелания и критику. Бросать разработку не собираюсь. Просто закончились идеи :)