AJL — компонент для загрузки JS и CSS файлов средствами JavaScript

от автора

Привет, Хабр!

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *