Сложные интерфейсы на javascript вместе Yahoo UI. Часть 8

October 28, 2008Comments Off on Сложные интерфейсы на javascript вместе Yahoo UI. Часть 8

Современную ajax библиотеку не возможно представить без поддержки загрузки файлов. И Yahoo UI не исключение: загрузка файлов выполняется с помощью специального flash-ролика, который мы должны предварительно внедрить в веб-страницу. Минусы, вызванные необходимостью поддержки браузером клиента нужной версии flash player-а, почти не заметны. А вот появление в списке дополнительных возможностей средств мониторинга за тем процентом выполнения операции - крайне приятно.

В прошлый раз я остановился над тем, что создал заготовку "ненастоящей" html-формы. Формы, состоящей из текстового поля (обычного поля, никакого элемента типа "file"), в котором должно отображаться имя выбранного файла перед его отправкой на сервер и двух кнопок: показать диалоговое окно выбора файла и начать загрузку файлов на сервер. После того как запрос на сервер ушел, мы начинаем самое интересное - ловим события прогресса загрузки, предварительно назначенное вызовом addListener:
  1. //назначаем слушатель события
  2. u.addListener('uploadProgress',onUploadProgress);
  3.  
  4. // и обрабатываем пришедшее событие
  5. function onUploadProgress (e) {
  6.  alert ("loaded "+e.bytesLoaded+" from "+ e.bytesTotal); 
  7. }
Объект события "e" содержит два интересующих нас свойства: bytesLoaded (сколько байт уже загружено на сервер) и bytesTotal (и сколько всего нужно загрузить). К сожалению, в YUI нет специального визуального компонента progressbar-а, однако сделать что-то похожее "на коленке" не сложно. После того как загрузка файла будет завершена, мы получим еще одно сообщение: uploadComplete. Большой пользы от него нет: объект события (e) содержит только свойство "id" с номером (например, file3) файла, загруженного на сервер. Если загружалось несколько файлов, то и функция слушатель будет вызвана несколько раз. Гораздо интереснее познакомиться с обработкой события uploadCompleteData:
  1. u.addListener("uploadCompleteData",onUploadResponse);
  2.  
  3. function onUploadComplete (e){
  4.   alert ("вот ответ сервера: "+e.data);
  5. }
Содержимым поля data является результат выполнения серверного скрипта. А теперь плохая новость: дело в том, что, как и метод обработки события uploadComplete, так и слушатель события uploadCompleteData вызываются для каждого из файлов. Т.е. фактически даже, если я делаю вызов на объекте YUI Uploader метода uploadAll, то сами файлы отправляются на сервер по отдельности, и отклики на них приходят также по отдельности. Следовательно, серверный скрипт будет вызван столько раз, сколько файлов загружается на сервер, и, в общем случае, не знает, текущий вызов "это уже все и можно делать какую-то работу" или "еще нет", и нужно подождать, например, еще два или три вызова пока дойдут оставшиеся файлы. Проблема эта не страшная и решается путем передачи на сервер в качестве дополнительного параметра числа, равного общему количеству файлов загружаемых на сервер. А серверный скрипт просто подсчитывает то, сколько файлов уже пришло и сравнивает их с ожидаемым числом. Когда эти два числа совпадают, это значит что загрузился последний файл и можно начинать собственно какую-то работу. Сделать такой алгоритм не сложно: просто храните в сессии два числа и правильно их обновляйте. Еще: учитывайте возможность двух одновременных, идущих параллельно загрузок нескольких файлов. И последнее: помните, что для flash-контента действует правило безопасности: если я загружаю uploader.swf с домена X, то выполнить операцию загрузки файла я могу только на этот домен "X" или на любой другой домен "Y", но для которого требуется, чтобы он "доверял" домену "X". Доверие для flash-а определяется наличием на сервере специального файлика crossdomain.xml, в котором перечисляется набор доверенных сайтов, например, так:
  1. <cross-domain-policy>
  2.   <allow-access-from domain="www.siteA.com" />
  3.   <allow-access-from domain="*.siteB.com" />
  4. </cross-domain-policy>
Вернемся назад к вопросу передачи на сервер, уже не файлов, а обычных текстовых переменных. Для этого в прошлых статьях я показал то, как использовать вызов метода YAHOO.util.Connect.asyncRequest. YUI содержит еще один модуль с нечетко сформулированным функционалом, и претензиями на поддержку загрузки данных в стиле ajax-а (и называется он Get). Такое описание звучит пусть и странно, но имеет свое обоснование. В далеком 2005 г. когда я писал свой первый сайт работающий на базе ajax, то столкнулся с проблемой: родная поддержка объекта XmlHttpRequest ("сердца" ajax) была только в браузере firefox. Что касается internet explorer-а (в то время актуальной версией была шестерка), то для того чтобы делать ajax вызовы, нужно было создать ActiveX объект "Microsoft.XMLHttp". Это было плохо не только из-за того, что приходилось проверять версию браузера и по различному работать с ajax-вызовами. Но главное то, что activeX не самая надежная с точки зрения internet и опасности заражения вирусами технология. Так что те пользователи, которые отключали activex, заодно отключали и возможность "видеть" ajax. А даже если не отключали, то internet explorer мог довести до "белого каления", когда на каждый ajax "чих" всплывало диалоговое окошко с вопросом, вы точно уверены, что хотите разрешить создание activeX объекта? Третий по популярности браузер - opera (в те годы это была версия 7) поддержки XmlHttpRequest-а вообще не содержала. Сейчас ситуация улучшилась: все последние версии основных браузеров давно поддерживают ajax как встроенную функциональность. Разумеется, кроме internet explorer 6, который благополучно "завис" в 2000 г., при этом используется на все еще значительной доле компьютеров "для домохозяек", и заставляет "скрежетать зубами" веб-программистов и дизайнеров. Попробуйте, например, открыть исходники файла connection.js (содержащего реализацию функции YAHOO.util.Connect.asyncRequest). И вы там найдете достаточное количество кода, проверяющего и по-особому обрабатывающего ситуацию "вернулся монстр из прошлого". В 2005 г. появилось множество javascript-библиотек, призванных работать в ie, opera и эмулировать отсутствующую ajax-функциональность. Делалось это либо за счет создания невидимого фрейма (iframe), внутри которого загружался результат выполнения php-скрипта на сервере (а он в свою очередь формировал данные либо в xml, либо в json формате). Использование iframe рождало целый ряд проблем связанных с usability. В частности браузер изменял историю навигации по странице, добавляя туда новые "виртуальные страницы" всякий раз, когда в iframe загружалась новая информация. Зато с помощью iframe можно было загружать на сервер ajax-ом файлы. Второй способ имитации ajax с файлами работать не умел, но и был свободен от недостатков iframe - проблемы "лишнего щелчка" и засорения истории. Идея была следующей. Все вы знаете, что для того, чтобы поместить на html-страницу некоторый javascript-код, необходимо создать элемент "script" и указать им на javascript-файл.
  1. <script src="logic.js"> </script>
Файл logic.js вовсе не обязательно должен был быть статическим файлом с javascript-кодом - это вполне может быть и php-скрипт. Главное, чтобы он результат его работы был оформлен по правилам json. Более того, необязательно чтобы элемент "script" присутствовал в коде html-странице изначально - мы можем создать его динамически и при этом передать серверному скрипту, генерирующему json-данные, некоторые переменные, например, так:
  1. <script src="logic.php?fio=Mark&age=12"> </script>
Все это звучит просто только на словах, на практике же из-за "маленьких особенностей" браузеров приходилось писать совсем не маленький код, который приводил все к общему знаменателю. Я с чувством глубокой признательности до сих пор вспоминаю библиотеку JsHttpRequest (как раз решавшей проблемы совместимости) от Д. Котерова (dklab.ru). Теперь попробуем написать пару строк кода с помощью YUI модуля Get и затем сформулируем что же в нем не то. Назначение Get - динамическая вставка в html-код страницы либо элементов link style либо script. Для примера я создал html-код странички с двумя кнопками и текстовой надписью:
  1. <h1>Hello</h1>
  2. <button onclick="doRed()">red</button>
  3. <button onclick="doBlack()">black</button>
По нажатию на кнопку doGet я хочу выполнить загрузку такого нового стилевого файла, чтобы оформление заголовка H1 изменилось и текст поменял цвет на красный. Предварительно я создал и разместил рядом с html-файлом следующий css-документ style.css:
  1. h1 { color: red; }
Теперь код функция обрабатывающих нажатия на кнопки смены цвета:
  1. var savedcss = null;
  2.  
  3. function doRed (){
  4.  var callback = {
  5.      onSuccess: function (e){
  6.           savedcss = e; 
  7.      }
  8.  };
  9.  YAHOO.util.Get.css (["style.css"], callback); 
  10. }
  11.  
  12. function doBlack (){
  13.  if (savedcss) 
  14.      savedcss.purge ();
  15. }
В качестве первого параметра методу YAHOO.util.Get.css я передаю массив строк с именами тех css-файлов, которые следует загрузить (style.css). Второй параметр - объект callback - служит для того, чтобы получить сообщение от YUI, как только запрошенные нами ресурсы будут успешно загружены. Внутри функции onSuccess я сохраняю ссылку на объект события "e". Эта переменная мне потребуется для того, чтобы вернуть цвет заголовка "обратно", сделать его черным. Ничтоже сумнящеся я пишу "e.purge()" (функция purge приводит к уничтожению динамически добавленного в html-страницу узла css link или script). Итак, вызвав purge, я ожидаю, что заголовок станет из красного черным, ведь стиль "style.css" должен выгрузиться. Запускаю, пробую: в firefox и opera все работает. Пробую в internet explorer 6 - после первого нажатия на кнопку "red" стиль заголовка поменялся, а вот назад он возвращаться не хочет. И ведь дело не в том, что internet explorer такой плохой браузер (в семерке приведенный выше пример кода отлично работает) - возможности по выгрузке css-стилей у него есть, просто прямолинейное уничтожение css-узла не помогает. Возможно, вы скажете, что это совсем не проблема и если мне нужен сайт с динамической сменой используемых css-стилей (один сайт с несколькими скинами). То нужно просто загружать все новые и новые файлы стилей, не выгружая старые? Да можно, но это грязный" код. А что еще умеет загружать get кроме css и script? Да ничего. Дело в том, что если авторы YUI хотели позиционировать get как универсальный инструмент динамической загрузки в html-страницу ресурсов, то не хватает поддержки, да хотя бы, файлов картинок. А как казалось, было бы приятно сказать yui: "загрузи мне эти десять картинок для меню, а когда будешь готов, то сообщи мне об этом, чтобы можно было эти картинки показать пользователю". Увы, так сделать нельзя. А может быть можно заместить содержимое некоторого блока div в странице динамически генерируемым содержимым (да хоть текстом новости, которую печатает скрипт news.php). Снова нельзя - нужно обращаться к YAHOO.util.Connect.asyncRequest. Хотя и jquery и mootools (об этих библиотеках я ранее писал статьи) такую функцию поддерживают. Так что ну никак модуль Get не "тянет" на роль универсального загрузчика ресурсов. Также может быть загрузка css - это всего лишь бесплатный довесок к возможности загружать javascript? Может быть, да, только сначала подумаем, зачем вообще нужна такая функция? Особенно сейчас, когда "родная поддержка" ajax есть во всех браузерах, а в YUI есть специализированный метод YAHOO.util.Connect.asyncRequest. Явно Get - это не способ дать браузерам трехлетней давности поддержку ajax, наподобие того, как делал jsHttpRequest. Все дело в том, что при выполнении ajax запросов с помощью XmlHttpRequest действует правило запрещающее javascript-кода, загруженному с сайта "a.com" обращаться к скрипту распложенному на сайте "b.com". А вот на загрузки тегов script такого ограничения нет. Так что вы можете из javascript обратиться к произвольному доверенному сайту (здесь это ключевое слово) за информацией. Фокус в том, что особенность работы script такова что, как только содержимое было загружено, то оно начинает выполняться и получает доступ ко всей информации, содержащейся в окне браузера. Это называется уязвимость XSS и это плохо. Плохо и еще то, что вы должны быть четко уверены, в какой форме отдаст json содержимое чужой сервер: если этот формат меняется, то ваш код обрабатывающий данные нужно менять. Значит, что загружать данные чуть ли не с любого сайта не получится. Скажете это очевидно, но тогда где плюсы Get относительно использования промежуточного proxy.php скрипта, размещенного на вашем сервере. Скрипт proxy может обращаться к любому другому ресурсу (в том числе с помощью метода post, и с загрузкой файлов). Затем результат выполнения того другого скрипта проверяется на предмет совместимостей, возможно, преобразуется в более удобный для анализа формат (или унифицированный промежуточный формат). Возможно, что перед отдачей результата своей работы proxy.php выполняет gzip-сжатие и другие оптимизации. Как вывод: модуль Get не тянет на роль универсального загрузчика ресурсов, а сфера его использования для обхода crossdomain ограничений довольна опасна. Это не значит, что Get бесполезен: я могу придумать множество ситуаций когда прямые crossdomain вызовы необходимы: например экономия трафика сервера, обход ограничений на количество запросов с одного ip-адреса. Но все же лучше без них.

Одна из известных и уже набивших оскомину проблем создания ajax-сайтов - это научить их корректно работать с адресной строкой браузера и его историей. Пользователь привык, что, нажимая на кнопки "вперед" и "назад", он будет перемещаться по истории и видеть различные состояния страницы. Привык к тому, что можно скопировать адрес страницы в буфер обмена, добавить его в закладки или послать по почте. Типовой же ajax-сайт выполняет модификацию содержимого страницы, загрузку новой информации, не изменяя ни адресной строки браузера, ни истории посещения сайта. Реализовать собственную функцию "запоминающую состояние" страницы не сложно. Для того чтобы отличать различные состояния веб-страницы друг от друга, в адресной строке к базовому имени страницы, например, "news.php", добавляется элемент "якорь", например, "news.php#finance" или "news.php#sport". Такие ссылки уже можно и поместить в менеджер закладок и послать по почте. Вам скрипт должен при начальной загрузке страницы проверить наличие в адресной строке "якоря" и выполнить загрузку соответствующего ему содержимого. Сделать это не сложно, но чтобы не изобретать велосипед лучше воспользоваться созданным разработчиками YUI модулем History. Для начала подключаем модуль history, снова с помощью yui loader-а:
  1. loader = new YAHOO.util.YUILoader();
  2. loader.require(["history"]);
  3. loader.loadOptional = true;
  4. loader.base = 'js/';
История навигации по ajax-страницам будет записываться с помощью объекта iframe. Как я писал уже выше, любое изменение содержимого iframe помещается браузером в историю. Естественно, что сам блок iframe нам визуально не нужен и поэтому будет сделан не видимым.
  1. <iframe id="yui-history-iframe" src="empty.html" style="width:0px; height:0px; display:none;"></iframe>
  2. <input id="yui-history-field" type="hidden" />
Рядышком с главным html-файлом я создал еще один - empty.html, содержимое которого в полном соответствии с названием, отсутствует. Сама ajax-страница представляет собой набор модулей (не обязательно визуальных компонент). Каждая компонента находится в некотором состоянии. Следует выполнить регистрацию этих компонент и их состояний внутри объекта YAHOO.util.History. При регистрации модуля, я должен указать название модуля, начальное состояние (т.е. то в котором модуль должен перейти сразу после открытия страницы) и функцию, обработчик события "состояние модуля должно быть изменено" (вызывается при навигации клиентом по истории браузера). Для имитации ajax-приложения я решил создать блок div, содержимое которого будет меняться с помощью javascript. Хотя значением будет случайное число, генерируемое с помощью Math.random, однако с той же долей успеха внутрь блока div может загружаться и информация с сервера ajax-ом:
  1. <div id="news" style="width: 200px; height: 100px; border: 1px solid black;" ></div>
  2. <button onclick="loadNews()">load news</button>
Теперь смотрим на код функции loadNews. При нажатии на кнопку будет сгенерировано очередное случайное число и зарегистрировано как новое состояние модуля mytextfield:
  1. function loadNews (){
  2.   var r = "state"+Math.random ();
  3.   YAHOO.util.History.navigate ('mytextfield', r); 
  4. }
Обратите внимание на то, что я не изменяю здесь содержимое div-блока. Также имя модуля вовсе не должно совпадать с именем какого-либо из визуальных блоков html-страницы (имя модуля - нечто виртуальное).

Теперь пора выполнить инициализацию менеджера истории. Для этого я внутри обработчика события yui loader-а "все javascript библиотеки были загружены" выполняю следующие шаги:
  1. var initial = YAHOO.util.History.getBookmarkedState ('mytextfield') || "state0";
  2. YAHOO.util.History.register ('mytextfield', initial, updateUI);
  3. YAHOO.util.History.initialize("yui-history-field", "yui-history-iframe");
  4. YAHOO.util.History.onReady(updateUI);
Сначала я вычислил значение переменной initial. Она будет хранить текущее состояние модуля "'mytextfield'". Если пользователь не первый раз открыл страницу, а например, воспользовался закладкой, то адресная строка будет выглядеть так:

myfile.html#mytextfield=state0.1291611

Здесь текст "state0.1291611" это и есть код состояния модуля. Естественно, нужно предусмотреть ситуацию первого открытия страницы, и в этом случае значение состояния будет "state0". Вторым шагом я регистрирую модуль mytextfield, указав его начальное состояние и функцию, которая вызывается в тот момент, когда состояние модуля изменилось (клиент нажал кнопку назад или вперед в истории браузера). Третий шаг - инициализация самого диспетчера Yahoo History: просто передайте при вызове метода initialize имена созданных ранее тегов iframe и hidden. Последний шаг - вызов зарегистрированной для модуля функции updateUI. Если этого не сделать, то сразу после открытия страницы мы не выполним перехода на сохраненное состояние.
  1. function updateUI (){
  2.  YAHOO.util.Dom.get('news').innerHTML = YAHOO.util.History.getCurrentState('mytextfield');
  3. }
Код функции updateUI тривиален: я обращаюсь к объекту YAHOO.util.History, прошу его сообщить мне текущее состояние модуля "'mytextfield'" и вывожу имя этого состояния в созданный шагом ранее html блок "news".