Стыкуем компоненты в 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 = 'component 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. Можно начинать использовать и писать отзывы.

    P.S. сайт JSX пока лежит (испугался хабраэффекта?), можно попробовать почерпнуть информацию из кэша гугла
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 59

      0
      «2 потока загрузки модулей: для dom-base-utils и для dom. „
      судя по коду “utils-base-dom»? или я ошибся?

      в примере есть yass-component-counter, но counter.js не грузится, я наверное не понял примера, для чего используется «yass-component-counter»?

      *а вообще — шикарно! это же практически язык js-макросов на базе css…
      awesome!
        0
        сорри, комментарий чуть ниже
        0
        да, спасибо, поправил. Там порядок «наследования» функционала: сначала самый простой, потом более сложные. Т.е. utils ничего не требует для загрузки, base требует utils, а dom требует base.

        А как проявляется то, что counter.js не грузится? Под каким браузером?
          0
          ff3.1
          firebug показывает что не грузится
          что он должен изменять?

          *dom-base-utils еще остался чуть ниже
            0
            если на странице есть 'Counter loaded' — Значит, загрузился :)
              0
              нету
                0
                проверь, плиз, сейчас. Классический race condition, однако, возник :)
                  +1
                  не работает
                    0
                    спасибо большое. Вполне возможно, что сбоит алгоритм выбора модулей по CSS-селектору. Буду искать тестовое окружение :)
                      0
                        0
                        я поставил 3.1 beta2 — у меня проблемы не наблюдалось :(
                          0
                          виновник безобразия найден — это AdBlock Plus
                          ключевое слово «counter» ему сильно не нравится :)
                          без него все в норме
                            0
                            ух, прикольно :) сейчас переименую модуль :)
                              0
                              теперь работает
          0
          пытаюсь прикрутить один тест
          не работает такое наименование «yass-component-/scripts/fontface.js»
          стоит сделать

          * кручу дальше, будет пример :)
            +1
            я а может багу нашел, тестовая страница отвечает обычно так
            ClientSide is loaded
            Counter is loaded
            но иногда и так
            ClientSide is loaded
            ClientSide is loaded
            ClientSide is loaded
            ClientSide is loaded
            Counter is loaded
            это нормально?
              0
              нет, ненормально. Хорошо бы браузер, в котором вылазит. Асинхронность — такая весчь…
                0
                FF3
                  0
                  угу, удалось поймать лог для IE. Буду смотреть, где стыковки разрушились :) Спасибо сетевым задержкам
              +1
              итак, обещанный пример:

              во-первых я немного изменил yass (сорри):
              «script.src=alias+».js";script.type=«text/javascript»;"

              во-вторых, я добавил в body такой класс:
              «yass-component-/scripts/fontface»

              в-третьих, я создал такой fontface.js:
              if (navigator.userAgent.indexOf('Windows NT 6.0') == -1) {
              $('body').css(«font-family», '«Calibri2»');
              }

              в четвертых, в css есть такое определение:
              @font-face {
              font-family: «Calibri2»;
              src: url("/content/calibri.ttf") format('truetype');
              }

              что имеем в итоге? в итоге, опльзовтаели Vista не грузят больше calibri.ttf размером в 300 килобайт (этот шрифт уже установлен в виста), другие юзеры шрифт грузят. Осталось добавить фильтрацию по браузерам, которые не поддерживают font-face и мы сократим траффик на порядок :)

              Ура, YASS!

              смотреть на nuke.wyob.ru/css3test/
                0
                1. _.ua = navigator.userAgent
                2. $('body').css(«font-family», '«Calibri2»') === _('body')[0].style.fontFamily = 'Calibri2'

                :)
                  0
                  :) спасиб, торопился особо не оптимизировал
                +1
                очень интересный подход. реально асинхронность достаточно сильно ускоряет первоначальное отображение.

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

                предлагаю примерно следующий вариант решения
                var Optimizer = function(ops){
                  var scope = ops.scope ? ops.scope : window;
                  scope[ops.name] = function(arg){
                    if (scope[ops.name].once) return;
                    scope[ops.name].once = true;
                    var text = null;
                    dax({url:ops.script, id:ops.script, destroy:true, async:false, callback: function(resp){
                      text = resp.responseText;
                    }})
                    SRAX.evalScript(text);
                  }
                }
                new Optimizer({scope:Ext, name:'NorthLayoutRegion', script:'/scripts/ext/separate/layoutregion.js'})
                new Optimizer({scope:Ext, name:'ContentPanel', script:'/scripts/ext/separate/contentpanel.js'})
                new Optimizer({scope:Ext.dd, name:'StatusProxy', script:'/scripts/ext/separate/statusproxy.js'})
                new Optimizer({scope:Ext.dd, name:'DragSource', script:'/scripts/ext/separate/ddzone.js'})
                new Optimizer({scope:Ext.dd, name:'DropTarget', script:'/scripts/ext/separate/ddzone.js'})


                * This source code was highlighted with Source Code Highlighter.

                это примеры из реального проекта, правда относительно давно писалось, во времена extjs v1.

                Суть в следующем:
                Optimizer — это так называемый proxy — создает заглушки классов, методов. Он знает в каком файле лежит нужный метод. При первом вызове метода вызывается его заглушка, которая догружает фал с асинхронном/синхронном режиме, выполняет загруженый скрипт (тут происходит перезапись заглушки на реальный метод, класс), выполняет повторно вызванный метод.
                При повторных вызовах методов, классов — вызываются уже не заглушки, а реальные методы, классы.

                P.S.: Код Optimizer примерный, чтобы понять принцип.
                  0
                  да, только нам придется знать все заглушки тогда :)
                  но подход интересный
                    0
                    суть в том чтобы сделать Optimizer нормальный, а заглушки разработчик сам определит.
                      0
                      кажется история делает виток, скоро будут полноценные аналоги динамических dll-библиотек
                        0
                        скоро будут полноценные ОСи в браузерах, не то что какие dll :)
                    0
                    ну и получится как на яндексе — сначала загружается страница, где ничего не работает, а потом несколько секунд грузятся десятки скриптов и стилей…
                      0
                      :) кому что нравится. Лично мне очень не хватало динамической загрузки счетчика от Гугла. А дерево приведено только для примера, никто не мешает загружать все в 2-3 файлах.
                        0
                        при 2-3 файлах совершенно не нужно городить яваскриптовый огород вокруг зависимостей, динамических подгрузок, волшебных стыковок компонентов друг с другом и прочей фигни.
                          0
                          совершенно верно. Но почему не подключить дополнительные полкилобайта кода, чтобы весь этот огород получить? Сейчас yass в пожатом виде занимает 3 Кб
                            0
                            только ты не учитываешь, что хтмл разрастается неимоверно, а надёжность всей этой конструкции падает ввиду увеличения её сложности.

                            вот, допустим, у тебя есть компонента «подтверждение при клике». пока она одна — подумаешь, прописать пару лишних классов с гигантскими именами, да запихнуть в тайтл (чтобы при наведении код показывал? о_0) текст с вопросом.
                            но что если нужно вывести табличку с кучей кнопочек «удаления с подтверждением»? получится гора копипасты.
                              +2
                              вообще говоря, пихать яваскрипт код в аттрибуты элементов — ничем не лучше, чем пихать пхп код в хтмл. ибо логика имеет свойство разрастаться и превращаться в длиннющую простыню…

                                0
                                зачем нам прописывать все в HTML? достаточно родителей модулей указать
                                  0
                                  всмысле?
                                    0
                                    Отвечая на предыдущий комментарий: у нас есть, фактически, три способа выполнить что-то «по загрузке» скрипта:
                                    1. Указать функцию в самом низу файлу модуля
                                    2. Указать обработчик для дерева модулей через _.modules[modules_alias].init
                                    3. Указать обработчик через атрибут title в HTML-коде

                                    Первые два способа никак не влияют на HTML-код. Третий — лишь для «быстрой» загрузки внешних модулей.

                                    Далее, если у нас есть какой-то компонент clientside, то мы можем привязать всю логику к class=«yass-component-clientside» — этот элемент будет родительским для блока, отвечающего за функционирования данного компонента.

                                    В самом примере классы в HTML вынесены только в демонстрационных целях. С таким же успехом можно указать дерево через _.load в конце скриптов (например, так сделано для yass.animation.js).
                                      0
                                      параметры модуля для конкретного узла всё-равно нужно будет тянуть простынёй…
                                        0
                                        что такое «параметры модуля для конкретного узла» и чем это будет отличаться от «обычного» JS-программирования? Можно пример?
                                          0
                                          ну вот, например, чуть выше я упомянул компоненту «переспросить». в зависимости от ссылки нужно задавать разные вопросы.
                                            0
                                            отлично. Все такие ссылки располагаются внутри контейнера, который загружает требуемый модуль. Модуль обходит все ссылки внутри контейнера и в зависимости от ссылки вешает какой-то обработчик.

                                            Или я снова что-то не понял?
                                              0
                                              представь таблицу в правой колонке которой располагаются кнопки удаления. в других ячейках — другие ссылки. общий контейнер для всех ссылок удаления — таблица. при клике на кнопку удаления должно выскакивать сообщение «вы действительно хотите удалить запись ?»
                                                0
                                                а чем это отличается от описанного примера?

                                                Проходит по таблице и навешиваем обработчики на ссылки удаления. По нажатию на ссылку у нас есть e.target, который позволяет получить все необходимые параметры. Чего нам еще не хватает?
                                                  0
                                                  для каждой ссылки удаления нужно формировать свой текст.
                                                  предлагаешь создавать по отдельной компоненте на «подтверждение удаления», «подтверждения бана», «подтверждения ещё чего-нибудь»?

                                                  емнип, обработчик клика нужно вешать на саму ссылку, иначе её уже будет не остановить…
                                                    0
                                                    но сейчас-то эти ссылка на «бан» и «удаление» как-то различаются? что мешает это различие и использовать?
                                                      0
                                                      предлагаешь внутри компоненты сделать гигантский свич с текстами для всех вариантов ссылок и кодом для вычленения названия удаляемого материала из окружающей эти кнопки разнообразной вёрстки? это уже будет далеко не 3кб…
                                                        0
                                                        а какое решение предлагаешь ты, что он не совместимо с описанным подходом?
                                                          0
                                                          никакое =)
                                                            0
                                                            ну, так не интересно :)

                                                            Просто моя позиция такова, что описанный подход не делает ничего лишнего. Он просто позволяет пользоваться тем, что уже было, более удобным образом. И все.
                                                              0
                                                              а моя — что в вёрстке не должно быть никаких скриптов — только данные. а скрипты уже сами должны разбираться с какими данными как работать. в частности, вместо того, чтобы давать элементам классы с именами модулей, лучше давать им классы с семантическими именами. а будет ли их «обогащать» один модуль, другой, или оба вместе — должны решать сами модули.

                                                              например, я недавно сделал такую фичу, что для любой текстарии можно указать аттрибут v:maxLength=«100» и к ней подключится два модуля — валидатор форм, не дающий засабмитить форму, если значение в текстарии превышает лимит, и корректор длинны, который автоматом удаляет лишние символы, не давая пользователю возможности превысить лимит. при этом они друг от друга не зависят и выход из строя одного никак не скажется на работоспособности другого. и если я их захочу объединить или наоборот разделить на ещё большее число частей — вёрстку переделывать не придётся, ибо там содержится вся необходимая и достаточная информация, без излишних имён модулей, их параметров и прочей вторичной инфы.
                                                                0
                                                                :) а если завтра придется сделать ограничение 120? Вводить параметры в HTML-элементы еще большее зло — тут уже никакой семантикой не прикроешься. А класс yass-module-textarea и(ли) restricted-length вполне себе семантичен.
                                                                  0
                                                                  ограничение длины определяет серверный экшен, который прописан в форме. форма, собственно, для того и предназначена, чтобы указывать что и в каком виде передавать на сервер. ес-сно ограничения, типы, имена и прочее по хорошему должны определяться моделью, а не хардкодиться в каждом шаблоне, но уж точно не стоит их задавать глобально для всего сайта в яваскрипте.

                                                                  зачем писать yass-module-textarea, если уже и так написано два тэга textarea? лучше уж yass-rich.

                                                                  и, кстати, yass пора переименовывать в yajf :-)
                                                                    0
                                                                    может тогда, jfyi или sdviusnvios? O_o

                                                                    нашу дискуссию можно вести бесконечно :)
                                                                      0
                                                                      ну, yet another css selectors твоему фреймворку уже совсем не подходит…

                                                                      кстати, как насчёт того, чтобы не захламлять глобальную область видимости своими переменными?

                                                                      мисье страдает дискуссофобиями? тогда добро подаловать к нам, на процидурки B-)
                                                                        0
                                                                        в глобальной области видимости всего 2 переменные: yass и _
                                                                          0
                                                                          не всего… а целых две переменные! =)
                                                                            0
                                                                            если _ уже определена, то вообще одна!
                        +2
                        Использовал похожий загрузчик в одном из разработанных проектов (1й метод из приведённых).
                        Страничка логина появлялась на экране сразу, что позволяло вводить username/password, пока грузятся компоненты (JS и CSS файлы).
                        Загрузка происходила за несколько секунд (подключались тяжеловесные компоненты: ExtJS, Google Maps, etc), этого времени как раз хватало, чтобы ввести пароль.

                        Результат подобного подхода налицо — мгновенная доступность сервиса для пользователя.
                        Время загрузки первой HTML странички минимально. Можно интегрировать стили в неё же (так делает Google, например).

                        Всё же, для большинства проектов подобный подход сродни «из пушки по воробьям».
                        Достаточно корректно прописать правила .htaccess (заголовок Expires), чтобы браузер закэшировал скрипты после первой загрузки.

                        Вывод: метод замечательный, но используем там, где необходимо (руководствуемся здравым смыслом).
                          +1
                          А вот мой js-loader на mootools:
                          var jsLoader = new Class({
                            Implements: Options,
                            
                            loadModel: {},
                            options: {  
                              version:   «0.1.1»,
                              path:     "/source/js/",
                              cache:     true
                            },
                            
                            initialize: function(files, options){
                              var self = this;
                              window.addEvent(«load», function (event) {
                              self.setOptions(options);
                              if(!Browser.Engine.trident){
                                console.time(«module load»);
                              }
                            
                              if($defined(files)){
                                files.each(function(file){
                                  self.load(file);
                                });
                              }
                              
                              if(!Browser.Engine.trident){
                                console.timeEnd(«module load»);
                              }
                              window.fireEvent(«loadMoule»);
                              });
                            },
                            
                            load: function(file){
                              var code = '';
                              if(this.loadModel[file] && this.loadModel[file] >= this.options.version) return false;
                              var js = new Asset.javascript(this.options.path+file+".js", { onload: function(){
                                window.fireEvent(file);
                              }});

                              this.loadModel[file] = this.loadModel[file]? Math.max(this.loadModel[file], this.options.version): this.options.version;
                            }  
                          });

                          * This source code was highlighted with Source Code Highlighter.
                            0
                            Давно занимался поиском таких скриптов. Просмотрел JsLoader, modules.js, lazyload, загрузчики под YUI, jQuery, Prototype.
                            Остановился на JSLoad (http://www.instructables.com/community/Free-Code-JSLoad/). Немного дописал и пользуюсь. Сначала определяешь зависимости, затем в нужном месте пишешь JSLoad.load('test', function() {alert('test loaded')}).
                            За JSX спасибо, попробую в бою.

                            Only users with full accounts can post comments. Log in, please.