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

Я продолжаю рассказ о возможностях Yahoo UI — как возможностях в построении красивых и удобных интерфейсов пользователей, так и возможностях YUI в "общении" с сервером (ajax). Прошлая статья носила достаточно теоретический характер: я рассуждал об основных проблемах построения приложений на ajax-платформе. Проблемах, характерных не только для YUI, но и для любой другой javascript-библиотеки. Сегодня я расскажу еще несколько "баек" о том, как правильно строить ajax-приложения, и покажу вам еще один способ загрузки файлов на сервер.

Прошлая статья была закончена на утверждении, что, хотя мы используем для "разговора" javascript-кода с сервером стандартные функции YUI, но не вызываем напрямую функцию YAHOO.util.Connect.asyncRequest, а создаем специальный объект Connector, играющий роль диспетчера асинхронных запросов. Т.к. в серьезных приложениях порядок выполнения запросов критически важен, а мы хотим избежать характерного для ajax-стиля разработки "перепутывания" порядка запросов к серверу и его ответов. Объект Connector содержит внутри себя очередь запросов и выполняет их строго один за другим. Идея с очередью запросов очень продуктивна и позволяет реализовать прием с "упорядочением" порядка выполнения запросов. Давайте для примера отойдем в сторону от веб-технологий и javascript к вещам более "железным": устройству жесткого диска компьютера или процессора. Когда мы даем компьютеру команду "прочитать файл A", то для этого ему нужно позиционировать головки винчестера над определенной областью диска. Если идут последовательные запросы на загрузку файлов A, B, C, D, расположенных в разных частях, то затраты на поиск этих файлов будут велики. С другой стороны, если переупорядочить запросы на чтение, например, так: B,C, A, D, — то можно избежать лишних перемещений головок. Устройство жесткого диска, несомненно, сложнее, но аналогия должна быть понятна. Также понятно то, что javascript-код не должен заниматься такими вещами, как изменение порядка запросов, а вот выполнить выбрасывание ненужных запросов из очереди — самое то. Предположим, что мы спроектировали следующий интерфейс. Веб-страница разделена на две части: таблица с перечнем некоторых заказов (в таблице выводится только небольшое количество полей), и внизу страницы будет расположена карточка заказа (а вот здесь выводится подробнейшая информация о заказе). Понятно, что мы не можем загрузить в страницу сразу всю информацию о заказе — это будет слишком медленно. Вместо этого мы загружаем только те характеристики заказа, которые необходимы для формирования таблички с их перечнем. Зато, как только какая-то строка была выделена (получила фокус), мы делаем запрос, который подгружает с сервера остальные сведения. Звучит просто, и подводных камней не видно? Как бы не так. Предположим, что сейчас выделена запись 1, работающий с нашей программой оператор хочет перейти к записи 11 и, просто прокручивая колесико мышки, заставляет фокус выделения текущей записи перейти на нужную позицию. Тем самым он 10 раз вызывает обработчик события "раз выделена новая запись — грузи сведения о ней сервера". Из этих десяти запросов девять совершенно не нужны, и их можно было бы не отправлять. Без четко выделенной очереди, где хранятся запросы к серверу перед их отправкой, сделать это было бы сложно. А так наш диспетчер запросов просматривает очередь и видит, что некоторые из запросов можно выкинуть, "сжать", оптимизировать. И примеров, подобных приведенному выше, можно придумать еще много.

Централизация обработки событий "отправлен запрос" и "получен ответ от сервера", дает возможность сделать интерфейс чуть "красивее". К примеру, при отправке запроса обработка его серверным скриптом и отправка ответа обратно занимает некоторое время. Чтобы пользователь не гадал, сработала система или нет, и не жал повторно на кнопки, нужно вывести подсказку "мол, запрос обрабатывается, подождите". Можно вывести в углу страницы надпись, снабженную анимированной картинкой в виде пересыпающихся песочных часов или чего-то похожего. Небольшая проблема в том, что, если идет отправка нескольких запросов, каждый из которых занимает относительно малое время, то появляется некрасивый визуальный эффект, когда картинка часов появляется, но не успевает стрелка переместиться хотя бы на пару делений, как нужно ее прятать (пришел быстрый ответ от сервера). А через секунду мы выполняем еще один запрос, и снова и снова часы мелькают на экране. При работе с единым диспетчером запросов такой проблемы нет: диспетчер не будет прятать анимацию загрузки, если в очереди еще есть необработанные запросы. Кроме того, запросы к серверу могут быть разными в плане совмещения их с поведением UI интерфейса. Например, вернувшись к примеру с таблицей заказов и их детализацией внизу страницы, можно представить такой сценарий работы. Оператор выбирает заказ, правит его характеристики и нажимает на кнопку сохранить и переходит к следующей записи. Очевидно, что операция сохранения могла быть неудачна: например, запрос к серверу потерялся — потерялся вместе с теми сведениями, которые ввел оператор. И когда диспетчер запросов понимает, что ответ от сервера не пришел в установленное время, то сообщает об этом оператору, чтобы он снова нажал на кнопку "сохранить". Однако оператор уже перешел на редактирование другой записи и потерял тем самым предшествующие правки. При отправке подобных критически важных запросов необходимо блокировать дальнейший ввод данных, например, выводя модальное диалоговое окно. Как это сделать с помощью класса YAHOO.widget.Panel, я рассказывал в четвертой части серии. В любом случае код, блокирующий и разблокирующий интерфейс приложения, лучше поместить в одно место — в код диспетчера запросов Connector. И это еще не все: к примеру, с помощью Connector'а легче реализовать умное кэширование запросов к серверу. Что такое умное кэширование, и чем оно отличается от просто кэширования ajax- запросов? Не секрет, что браузер кэширует запросы к серверу, если они не отличаются передаваемыми серверному скрипту параметрами. На кэширование запросов также влияют значения заголовков Cache-Control и Expires (их можно установить с помощью php-функции header), но мы пока не будем учитывать влияние заголовков. Для того, чтобы браузер не кэшировал запросы, используется финт с добавляемым к списку передаваемых на сервер переменных еще одной фиктивной переменной, значение для которого генерируется случайным образом — например, так:
get_info_about_user.php?fio=Jim&rand=0.45435

Таким образом, мы, хотя и избавимся от проблемы устаревания информации, однако получим взамен другую проблему — избыточные запросы. Например, если оператор просто просматривает информацию (перемещаясь вверх-вниз по таблице заказов) без редактирования. Тогда нет смысла перечитывать сведения от сервера. С другой стороны, если была выполнена операция редактирования и сохранения изменений, то кэшем обойтись нельзя, и нужно перечитать изменения с сервера для всех тех таблиц и записей, которые потенциально могли быть затронуты проведенной правкой. Опять-таки, эти действия носят общий характер, и не стоит их "разбрасывать" по различным частям javascript-приложения — давайте, как и ранее, поместим логику в класс Connector. Идея центрального диспетчера запросов применима и для javascript-, и для flash/flex-приложений. Особый вкус она приобретет, если вы будете строить свое приложение на идеях event-driven-проектирования и MVC. Т.е. весь ваш пользовательский интерфейс: кнопки, менюшки, таблички, — как и раньше, отображают информацию и имеют обработчики событий. Однако внутри той же функции "нажали на кнопку редактирования" нет никакой логики изменения внешнего вида связанных компонентов и отправки запроса (пусть и с помощью нашего Connector'а) на сервер. Вместо этого создается объект "Команда" — этот объект наполняется сведениями, нужными для выполнения запрошенного действия (номер товара, выбранного для правки). И затем эта команда поступает в очередь на обработку специально выделенным диспетчером событий. Диспетчер же знает, что при поступлении события "Правка товара" нужно выполнить следующий сценарий работы: изменить внешний вид определенных UI-компонентов и отправить запрос для обслуживания на сервер. Можно декларативно записать перечень возможных состояний системы: просмотр заказов, редактирование, удаление. К каждому из таких состояний привязать определенные свойства визуальных элементов управления (надписи, свойство enabled для кнопок и т.д.). Затем вы описываете набор правил переходов, т.е. при поступлении какого события нужно перейти в какое состояние. Обязательно нужно избавиться от таких артефактов, как глобальные функции и переменные, для связывания между собой нескольких частей или модулей, из которых состоит ваше javascript- приложение. Вместо этого используйте "выбрасывание" бизнес-событий (начато редактирование заказа, начата процедура оформления документов и т.д.), несмотря на довольно большой объем работы по созданию среды для дальнейшей разработки приложения (и, скажем прямо, кажущейся совершенно бесполезной для javascript-решений). Тем не менее, наличие подобной функциональности для javascript-библиотеки я считаю более важной, чем огромное количество красивых и стильных элементов управления, т.к. отсутствующие "красивые" элементы управления будут неизбежно создаваться в ходе работы над крупным проектом, а вот поменять стиль проектирования и написания кода так легко не получится. Теперь главный вопрос: а где примеры кода такого волшебного класса диспетчера запросов Connector'а? А их и не будет, т.к. идеи на то и предназначены, чтобы каждый мог реализовать их сам с учетом своих специфических потребностей. На этом все и переходим к следующей теме: загрузка файлов и работа с crossdomain- ресурсами.

При загрузке файлов одним из важнейших вопросов является отслеживание прогресса выполнения этой операции. Грубо говоря, когда файл посылается ajax'ом на сервер, то мы хотели бы вывести для пользователя подсказку: сколько байт было загружено на сервер и сколько осталось. Показанная в 5- й части методика работы с файлами (с помощью объекта YAHOO.util.Connect.) такого не позволяла. Еще год назад, когда я написал серию статей про ajax, я рассказывал о двух методиках решения этой задачи. И лучшим выходом было использование для загрузки файлов специального flash-ролика, благо в flash начиная с 8-й версии появился специальный класс FileReference. В библиотеке YUI есть компонент YAHOO.Widget.Uploader, являющийся надстройкой над FileReference. Для следующего примера я сначала выполнил подключение модуля uploader (как всегда, воспользовался классом YUI- Loader):
loader = new YAHOO.util.YUILoader();
loader.require(["uploader"]);
loader.loadOptional = true;
loader.base = 'js/';
loader.insert({ onSuccess: loadCompleted});

В коде html-страницы должен присутствовать div-блок, внутрь которого и будет вставлен swf-ролик, и ресурсы, нужные для его работы.
<div id="uploader"> </div>

Содержимое блока, несмотря на то, что через секунду будет затерто, играет большую роль. Дело в том, что внутри элемента управления uploader "бьется пламенное сердце" другой известной javascript-библиотеки — swfobject. Те, кто профессионально работают с flash, знают, что swfobject предназначена для внедрения внутрь html-страницы flash-роликов. Выгодно отличаясь при этом от громоздких тегов embed & object, swfobject умеет внедрять flash-контент с учетом версии flash-плейера, есть поддержка технологии expressInstall, удобно передавать параметры внутрь swf-ролика, управлять его атрибутами визуализации (bgcolor, wmode, width & height). Достоинств у swfobject настолько много, что он стал в каком-то роде стандартом, пусть и неофициальным. Важно, что swfobject перед внедрением ролика проверяет наличие поддержки на странице flash (для работы YUI uploader'а необходима версия flash 9.0.45 и выше), и, если минимальные требования не выполнены, содержимое блока остается без изменения. Так что, заботясь о пользователях, рекомендую создать два альтернативных представления функции загрузки файлов на сервер: первое, классическое, построенное на использовании обычного тега <input type="file" />, и второй, улучшенный, с помощью YUI uploader'а. Сам блок div лучше всего сделать нулевого размера, в противном случае на странице появится огромное текстовое поле, в которое YUI uploader будет выводить журнал выполняемых действий (выбран файл, начата отправка). Теперь нужно добавить на страницу две кнопки: первая из них служит для того, чтобы вызывать диалоговое окно выбора файла (browse), вторая отправляет выбранный файл на сервер (upload). Рядом с этими кнопками можно разместить текстовое поле, в котором будет помещаться имя выбранного файла перед загрузкой на сервер.
<input type="text" name="file" id="file" disabled="disabled" />browse</button>
<button onclick="doBrowse()">browse</button>
<button onclick="doUpload()">upload</button>

Я специально включил параметр disabled для текстового поля "file", чтобы пользователь не попытался ввести "ручками" в него имя файла — выбор файла возможен только при нажатии на кнопку "browse". Теперь я приведу пример кода, который создает сам компонент YUI uploader. Обратите внимание на то, что мне пришлось явно присвоить свойству YAHOO.widget.Uploader.SWFURL путь к swf-файлу.
YAHOO.widget.Uploader.SWFURL = 'js/uploader/assets/uploader.swf';
u = new YAHOO.widget.Uploader( "uploader" );
// а теперь назначаем много-много обработчиков событий
u.addListener('fileSelect',onFileSelect)
u.addListener('uploadStart',onUploadStart);
u.addListener('uploadProgress',onUploadProgress);
u.addListener('uploadCancel',onUploadCancel);
u.addListener('uploadComplete',onUploadComplete);
u.addListener('uploadCompleteData',onUploadResponse);
u.addListener('uploadError', onUploadError);

Создание Uploader'а тривиально: передайте в качестве параметров конструктору идентификатор того html-блока, внутрь которого и будет внедрен swf- ролик. Еще нужно назначить обработчики событий для uploader'а. Я скопировал приведенный выше список из справки YUI, чтобы показать, на какие вообще события можно реагировать. В практике же достаточно обработки событий: onFileSelect (пользователь выбрал какой-то файл или несколько файлов). Событие onUploadProgress позволит "мониторить" прогресс загрузки файлов на сервер. А события onUploadComplete и onUploadError сообщат, когда код загрузки был завершен либо успешно, либо произошел какой-то сбой. Теперь пример кода функции doBrowse. Предварительно с помощью функции clearFileList следует очистить список загружаемых файлов. В противном случае при многократном вызове browse набор выбранных файлов будет все увеличиваться и увеличиваться.
function doBrowse (){
u.clearFileList();
u.browse(true, [{description:"Pics", extensions:"*.png; *.jpg; *.gif"}]); }

В качестве первого параметра при вызове метода browse я указываю признак того, можно ли выбрать для загрузки один (false) или несколько файлов (true). Второй параметр кодирует сведения о фильтрах. Массив фильтров (внимание на квадратные скобки) состоит из ряда объектов фильтров, для каждого из которых есть параметр description (просто надпись) и extensions — здесь вы через точку с запятой перечислите расширения допустимых файлов. Откровенно говоря, пользователь может явно ввести в поле фильтра диалогового окна желаемые маски файлов и выбрать файл с типом, отличным от задекларированного вами. Как только файлы были выбраны, генерируется событие "fileSelect", и вот пример обрабатывающей его функции: //нужна глобальная переменная
var files = null;
function onFileSelect(event) {
for (var i in event.fileList)
alert (files[i].name); }

Входной параметр для функции — event — содержит сведения о выбранных файлах (свойство fileList). Я организую цикл по этому объекту; т.к. fileList — это не массив, а объект со свойствами "file1", "file2", то нужно использовать именно цикл "for in". Каждый объект files[i] содержит ряд свойств — характеристик файла. Кроме показанного выше атрибута name (имя файла), есть еще size (размер файла в байтах), cDate (дата создания файла), mDate (дата модификации), id (какой-то уникальный номер). Среди всех этих свойств наиболее полезны name и size. Как известно, php- скрипты ограничены в объеме файлов, которые можно загружать на хостинг, а так перед началом загрузки вы можете сообщить пользователю об ошибке. Если же использовать классический "<input type=file>", то такой возможности нет. Я специально создал глобальную переменную files, которой присваиваю список выбранных файлов. Дело в том, что в функции doUpload (когда потребуется отправить файлы на сервер) нет способа через переменную "u" (ссылка на объект Uploader) добраться до того, какие файлы были только что выбраны (а значит, список файлов нужно сохранить заранее):
function doUpload() {
if (files != null){
for(var i in files) {
u.upload(i, >сайт 'get', {fio: 'Mark', age: 12*Math.random()}, 'file_fld');
} } }

Здесь мы видим все тот же знакомый нам цикл по перечню выбранных файлов. У объекта Uploader есть три метода, управляющих загрузкой файлов. Показанный выше метод upload служит для загрузки файлов по отдельности. Обратите внимание на то, что первым параметром я передаю методу upload ссылку на объект-файл, второй параметр кодирует полный http-адрес страницы. Можно указать адрес и так: " /myscript/post.php " (внимание на лидирующий слэш). А вот использовать относительные пути нельзя. Третий параметр — метод отправки (get или post). Нет, я не шучу: в действительности файлы отправляются всегда методом post, но вот если мы передаем скрипту еще дополнительные параметры (в примере переменные fio и age), то способ отправки влияет только на них. Последний аргумент метода — "file_fld" — задает имя поля, под которым файл будет загружаться. В случае, если вы хотите выполнить загрузку сразу всех файлов, используйте uploadAll. Он имеет такие же параметры, как и его "младший брат" — нет только первого параметра.

Нам осталось буквально "вот столечко", чтобы завершить рассмотрение того, как YUI работает с ajax и загружает файлы, но все это в следующий раз.

black-zorro@tut.by, black-zorro.com


Компьютерная газета. Статья была опубликована в номере 37 за 2008 год в рубрике программирование

©1997-2024 Компьютерная газета