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