После заметки Стыкуем асинхронные скрипты и предложенного решения от Steve Souders я подумал о модульной загрузке какого-то сложного JavaScript-приложения. И понял, что предложенный подход в таком случае будет довольно громоздким: нам нужно будет в конец каждого модуля вставлять загрузчик следующих модулей. А если нам на разных страницах требуются различные наборы модулей и разная логика их загрузки? Тупик?
Ан нет. Не зря Steve упоминает в самом начала своей заметки о событии onload
/ onreadystatechange
для скриптов. Используя их, мы можем однозначно привязать некоторый код к окончанию загрузки конкретного модуля. Дело за малым: нам нужно определить этот самый код каким-либо образом.
В качестве наиболее простого способа определить порядок загрузки модулей на конкретной странице можно предложить глобальный массив, содержащий в себе дерево зависимостей. Например, такое:
var modules = [ [0, 'item1', function(){ alert('item1 is loaded'); }], [1, 'item2', function(){ alert('item2 is loaded'); }], [1, 'item3', function(){ alert('item3 is loaded'); }] ];
В качестве элемента этого массива у нас выступает еще один массив. Первым элементом идет указание родителя (0
в том случае, если элемент является корнем и должен быть загружен сразу же), далее имя файла или его алиас. Последней идет произвольная функция, которую можно выполнить по загрузке.
Давайте рассмотрим, каким образом можно использовать данную структуру:
/* перебор и загрузка модулей */ function load_by_parent (i) { i = i || 0; var len = modules.length, module; /* перебираем дерево модулей */ while (len--) { module = modules[len]; /* и загружаем требуемые элементы */ if (!module[0]) { loader(len); } } } /* объявляем функцию-загрузчик */ function loader (i) { var module = modules[i]; /* создаем новый элемент script */ var script = document.createElement('script'); script.type = 'text/javascript'; /* задаем имя файла */ script.src = module[1] + '.js'; /* задаем текст внутри тега для запуска по загрузке */ script.text = module[2]; /* запоминаем текущий индекс модуля */ script.title = i + 1; /* выставляем обработчик загрузки для IE */ script.onreadystatechange = function() { if (this.readyState === 'loaded') { /* перебираем модули и ищем те, которые нужно загрузить */ load_by_parent(this.title); } }; /* выставляем обработчик загрузки для остальных */ script.onload = function (e) { /* исполняем текст внутри тега (нужно тольно для Opera) */ if (/opera/i.test(navigator.userAgent)) { eval(e.target.innerHTML); } /* перебираем модули и ищем те, которые нужно загрузить */ load_by_parent(this.title); }; /* прикрепляем тег к документу */ document.getElementsByTagName('head')[0].appendChild(script); } /* загружаем корневые элементы */ load_by_parent();
Мы можем вынести загрузку корневых элементов в событие загрузки страницы, а сами функции — в какую-либо библиотеку, либо объявлять прямо на странице. Задавая на каждой странице свое дерево, мы получаем полную гибкость в асинхронной загрузке любого количества JavaScript-модулей. Стоит отметить, что зависимости в таком случае разрешаются «от корня — к вершинам»: мы сами должны знать, какие базовые компоненты загрузить, а потом загрузить более продвинутые.
Кроме того, я вспомнил, что подобной проблемой уже занимался Андрей Сумин и даже предложил свое решение в виде библиотеки JSX, которая позволяет назначать список зависимостей через DOM-дерево. Для этого у элементов, которые требуют загрузки каких-либо модулей для взаимодействия с пользователем, назначается класс с префиксом jsx-component
, а далее идет уже список компонентов. Сама библиотека обходит DOM-дерево, находит все модули, которые нужно загрузить, и последовательно их загружает. Просто замечательно.
Но что, если нам требуется поменять обработчик по загрузке этого модуля? Как его задавать? Сама JSX использует атрибуты искомых узлов DOM-дерева сугубо для определения параметров этих модулей. Это достаточно удобно: ведь таким образом можно назначить и инициализатор модуля.
Также библиотека позволяет отслеживать повторную загрузку модулей, осуществлять догрузку модулей в случае плохого соединения и даже объединять разные модули в один исходный файл через систему алиасов. Таким образом, проблема асинхронной загрузки произвольного дерева модулей оказывается решенной. В случае JSX задача разрешается в обратном порядке: мы указываем основной файл (вершину дерева зависимостей), а он уже загружает все необходимые ему модули либо проверяет, что модули загружены.
Это все?
Почти. После недолгих раздумий JSX была взята за основу для построения модульной системы, которая могла бы стать основой для гибких и динамических клиентских приложений. Удалось совместить оба описанных выше подхода, что обеспечило все видимые функциональные требования к такого рода системе.
Для примера можно рассмотреть следующий участок HTML-кода:
<div id="item1" class="yass-module-utils-base-dom"> <span id="item2" class="yass-module-dom" title="_('#item2')[0].innerHTML = 'module is loading...';"></span> </div>
Давайте разберемся, какую логику загрузки он обеспечивает:
yass-module-*
.utils-base-dom
и для dom
. Причем в последнем случае загрузки, фактически, не будет: загрузчик дождется, пока состояние компонента dom
будет выставлено в loaded
, а только потом запустит (черeз eval
) код, записанный в title
этого элемента (в данном случае это span
).yass.dom.js
, yass.base.js
и yass.utils.js
. По загрузке всех этих модулей (ибо они вызваны в цепочке зависимостей, в данном случае dom
зависит от base
, который зависит от utils
) будет вызваны соответствующие инициализационные функции (если они определены). Таким образом возможны два типа обработчиков: непосредственно по загрузке компонента (будет вызвано для всех компонентов в цепочке) и после загрузки всей заданной цепочки компонентов (в нашем случае это utils-base-dom
).base-callbacks
), которая «заморозит» загрузку модуля base
до получения callbacks
. Сделать это можно (имея в виду, что расширяем зависимости модуля base
) следующим образом:_.load('callbacks-base');
yass-module-callbacks-base
. Это добавит в дерево зависимостей искомую цепочку.Для большей ясности описанное выше конечное дерево загружаемых модулей можно представить так:
dom -> base -> utils -> callbacks
Более того, на следующей страницы представлен процесс загрузки более сложного варианта дерева. Осторожно: задержек никаких нет, поэтому может работать очень быстро :)
Естественно, весь указанный функционал уже добавлен в последнюю версию YASS. Можно начинать использовать и писать отзывы.