После заметки Стыкуем асинхронные скрипты и предложенного решения от Steve Souders я подумал о модульной загрузке какого-то сложного JavaScript-приложения. И понял, что предложенный подход в таком случае будет довольно громоздким: нам нужно будет в конец каждого модуля вставлять загрузчик следующих модулей. А если нам на разных страницах требуются различные наборы модулей и разная логика их загрузки? Тупик?
Ан нет. Не зря Steve упоминает в самом начала своей заметки о событии
В качестве наиболее простого способа определить порядок загрузки модулей на конкретной странице можно предложить глобальный массив, содержащий в себе дерево зависимостей. Например, такой:
В качестве элемента этого массива у нас выступает еще один массив. Первым элементом идет указание родителя (
Давайте рассмотрим, каким образом можно использовать данную структуру:
Мы можем вынести загрузку корневых элементов в событие загрузки страницы, а сами функции — в какую-либо библиотеку, либо объявлять прямо на странице. Задавая на каждой странице свое дерево, мы получаем полную гибкость в асинхронной загрузке любого количества JavaScript-модулей. Стоит отметить, что зависимости в таком случае разрешаются «от корня — к вершинам»: мы сами должны знать, какие базовые компоненты загрузить, а потом загрузить более продвинутые.
Кроме того, я вспомнил, что подобной проблемой уже занимался Андрей Сумин и даже предложил свое решение в виде библиотеки JSX, которая позволяет назначать список зависимостей через DOM-дерево. Для этого у элементов, которые требуют загрузки каких-либо модулей для взаимодействия с пользователем, назначается класс с префиксом
Но что, если нам требуется поменять обработчик по загрузке этого модуля? Как его задавать? Сама JSX использует атрибуты искомых узлов DOM-дерева сугубо для определения параметров этих модулей. Это достаточно удобно: ведь таким образом можно назначить и инициализатор модуля.
Также библиотека позволяет отслеживать повторную загрузку модулей, осуществлять догрузку модулей в случае плохого соединения и даже объединять разные модули в один исходный файл через систему алиасов. Таким образом, проблема асинхронной загрузки произвольного дерева модулей оказывается решенной. В случае JSX задача разрешается в обратном порядке: мы указываем основной файл (вершину дерева зависимостей), а он уже загружает все необходимые ему модули либо проверяет, что модули загружены.
Это все?
Почти. После недолгих раздумий JSX была взята за основу для построения модульной системы, которая могла бы стать основой для гибких и динамических клиентских приложений. Удалось совместить оба описанных выше подхода, что обеспечило все видимые функциональные требования к такого рода системе.
Для примера можно рассмотреть следующий участок HTML-кода:
Давайте разберемся, какую логику загрузки он обеспечивает:
Для большей ясности описанное выше конечное дерево загружаемых модулей можно представить так:
Более того, на следующей страницы представлен процесс загрузки более сложного варианта дерева. Осторожно: задержек никаких нет, поэтому может работать очень быстро :)
Естественно, весь указанный функционал уже добавлен в последнюю версию YASS. Можно начинать использовать и писать отзывы.
P.S. сайт JSX пока лежит (испугался хабраэффекта?), можно попробовать почерпнуть информацию из кэша гугла
Ан нет. Не зря 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-модулей. Стоит отметить, что зависимости в таком случае разрешаются «от корня — к вершинам»: мы сами должны знать, какие базовые компоненты загрузить, а потом загрузить более продвинутые.
Решение второе: загрузка через DOM-дерево
Кроме того, я вспомнил, что подобной проблемой уже занимался Андрей Сумин и даже предложил свое решение в виде библиотеки JSX, которая позволяет назначать список зависимостей через DOM-дерево. Для этого у элементов, которые требуют загрузки каких-либо модулей для взаимодействия с пользователем, назначается класс с префиксом
jsx-component
, а далее идет уже список компонентов. Сама библиотека обходит DOM-дерево, находит все модули, которые нужно загрузить, и последовательно их загружает. Просто замечательно.Но что, если нам требуется поменять обработчик по загрузке этого модуля? Как его задавать? Сама JSX использует атрибуты искомых узлов DOM-дерева сугубо для определения параметров этих модулей. Это достаточно удобно: ведь таким образом можно назначить и инициализатор модуля.
Также библиотека позволяет отслеживать повторную загрузку модулей, осуществлять догрузку модулей в случае плохого соединения и даже объединять разные модули в один исходный файл через систему алиасов. Таким образом, проблема асинхронной загрузки произвольного дерева модулей оказывается решенной. В случае JSX задача разрешается в обратном порядке: мы указываем основной файл (вершину дерева зависимостей), а он уже загружает все необходимые ему модули либо проверяет, что модули загружены.
Это все?
Решение третье: JSX + YASS
Почти. После недолгих раздумий JSX была взята за основу для построения модульной системы, которая могла бы стать основой для гибких и динамических клиентских приложений. Удалось совместить оба описанных выше подхода, что обеспечило все видимые функциональные требования к такого рода системе.
Для примера можно рассмотреть следующий участок HTML-кода:
<div id="item1" class="yass-module-utils-base-dom"> <span id="item2" class="yass-module-dom" title="_('#item2')[0].innerHTML = 'component is loading...';"></span> </div>
Давайте разберемся, какую логику загрузки он обеспечивает:
- YASS при инициализации обходит DOM-дерево документа и выбирает все узлы с классом
yass-module-*
. - После этого формируется 2 потока загрузки модулей: для
utils-base-dom
и дляdom
. Причем в последнем случае загрузки, фактически, не будет: загрузчик дождется, пока состояние компонентаdom
будет выставлено вloaded
, а только потом запустит (черeзeval
) код, записанный вtitle
этого элемента (в данном случае этоspan
). - Первый поток загрузки асинхронно вызовет 3 файла с сервера:
yass.dom.js
,yass.base.js
иyass.utils.js
. По загрузке всех этих модулей (ибо они вызваны в цепочке зависимостей, в данном случаеdom
зависит отbase
, который зависит отutils
) будет вызваны соответствующие инициализационные функции (если они определены). Таким образом возможны два типа обработчиков: непосредственно по загрузке компонента (будет вызвано для всех компонентов в цепочке) и после загрузки всей заданной цепочки компонентов (в нашем случае этоutils-base-dom
). - Если мы хотим каким-то образом расширить нашу цепочку, то может в конце каждого из указанных файлов прописать загрузку какой-либо другой цепочки (например,
base-callbacks
), которая «заморозит» загрузку модуляbase
до полученияcallbacks
. Сделать это можно (имея в виду, что расширяем зависимости модуляbase
) следующим образом:
_.load('callbacks-base');
- Предыдущий шаг может быть также выполнен при помощи самого DOM-Дерева: нам нужно будет прописать для произвольного элемента класс
yass-module-callbacks-base
. Это добавит в дерево зависимостей искомую цепочку.
Для большей ясности описанное выше конечное дерево загружаемых модулей можно представить так:
dom -> base -> utils -> callbacks
Более того, на следующей страницы представлен процесс загрузки более сложного варианта дерева. Осторожно: задержек никаких нет, поэтому может работать очень быстро :)
Естественно, весь указанный функционал уже добавлен в последнюю версию YASS. Можно начинать использовать и писать отзывы.
P.S. сайт JSX пока лежит (испугался хабраэффекта?), можно попробовать почерпнуть информацию из кэша гугла