Статьи Архив статей

Автор: Мациевский Николай aka sunnybear
Опубликована: 15 января 2009

Стыкуем компоненты в JavaScript

После заметки Стыкуем асинхронные скрипты и предложенного решения от 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-модулей. Стоит отметить, что зависимости в таком случае разрешаются «от корня — к вершинам»: мы сами должны знать, какие базовые компоненты загрузить, а потом загрузить более продвинутые.

Решение второе: загрузка через 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 = 'module is loading...';"></span>
</div>

Давайте разберемся, какую логику загрузки он обеспечивает:

  1. YASS при инициализации обходит DOM-дерево документа и выбирает все узлы с классом yass-module-*.
  2. После этого формируется 2 потока загрузки модулей: для utils-base-dom и для dom. Причем в последнем случае загрузки, фактически, не будет: загрузчик дождется, пока состояние компонента dom будет выставлено в loaded, а только потом запустит (черeз eval) код, записанный в title этого элемента (в данном случае это span).
  3. Первый поток загрузки асинхронно вызовет 3 файла с сервера: yass.dom.js, yass.base.js и yass.utils.js. По загрузке всех этих модулей (ибо они вызваны в цепочке зависимостей, в данном случае dom зависит от base, который зависит от utils) будет вызваны соответствующие инициализационные функции (если они определены). Таким образом возможны два типа обработчиков: непосредственно по загрузке компонента (будет вызвано для всех компонентов в цепочке) и после загрузки всей заданной цепочки компонентов (в нашем случае это utils-base-dom).
  4. Если мы хотим каким-то образом расширить нашу цепочку, то может в конце каждого из указанных файлов прописать загрузку какой-либо другой цепочки (например, base-callbacks), которая «заморозит» загрузку модуля base до получения callbacks. Сделать это можно (имея в виду, что расширяем зависимости модуля base) следующим образом:
    _.load('callbacks-base');
  5. Предыдущий шаг может быть также выполнен при помощи самого DOM-Дерева: нам нужно будет прописать для произвольного элемента класс yass-module-callbacks-base. Это добавит в дерево зависимостей искомую цепочку.

Для большей ясности описанное выше конечное дерево загружаемых модулей можно представить так:

dom
  -> base
      -> utils
      -> callbacks

Более того, на следующей страницы представлен процесс загрузки более сложного варианта дерева. Осторожно: задержек никаких нет, поэтому может работать очень быстро :)

Естественно, весь указанный функционал уже добавлен в последнюю версию YASS. Можно начинать использовать и писать отзывы.

Читать дальше

Все комментарии (habrahabr.ru)