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

YUI — известная javascript-библиотека для построения "богатых" пользовательских интерфейсов веб-страниц. Однако ее изобразительные средства были бы малополезны, если бы в состав YUI не входили специальные модули, позволяющие загружать в html-страницу информацию с сервера. Так, в прошлый я начал рассказ о том, как YUI поддерживает идеи ajax. Мы научились загружать и отправлять на сервер информацию в различных форматах (text, xml, json) и даже смогли асинхронно отправить на сервер файл. Сегодняшняя статья "зашлифует" некоторые аспекты построения ajax-сайтов.

AJAX считается одним из способов уменьшить нагрузку на сайт. Так, может быть, вы уже получали "письма счастья" от вашего провайдера с просьбой или уменьшить нагрузки на сайт или переходить на отдельный тарифный план. Еще ajax позволяет уменьшить объем трафика, передаваемый по сети (вы ведь уже не верите в сказку о бесплатном unlimited-трафике для хостинга за 100 у.е. в год). Кроме того, ajax позволяет уменьшить время ожидания отклика приложения (страницы) в ответ на какое-то действие пользователя. И, наконец, ajax — это просто модно. В общем, куда ни кинь, без ajax'а никуда. Вся беда в том, что ajax стиль разработки имеет несколько недостатков, диктуемых своей природой. И если вы используете ajax для отображения результатов поиска по сайту Васи Пупкина, это одно, а если на ajax построено приложения для заказчика из буржуинии, и за деньги, то это — совсем другое дело.

Ajax расшифровывается как asynchronous javascript and xml. В слове javascript никаких подвохов нет, так же, как и в xml (с куда большим успехом сейчас вместо xml применяется json). А вот первая буква "A" (asynchronous) принесет нам массу неприятностей. Есть известное философское утверждение, что "недостатки — это продолжения достоинств", равно как верно и обратное. И javascript-, и flash-стили разработки по своей природе асинхронны. Т.к. работа выполняется в internet, и любая пара "запрос-ответ" передается по сети, то есть имеется какая-то доля вероятности того, что в ходе передачи что-то "поломается". Например, когда вы открываете любую страницу в любом браузере, то он определяет список размещенных на ней ресурсов (картинок, css-стилей) и запускает несколько процессов, которые одновременно загружают информацию. Альтернативный вариант, когда все ресурсы грузятся последовательно друг за другом, совсем плох: так, если в середине очереди загружаемых файлов попадется одна большая картинка или картинка, расположенная на медленном канале сети, то все остальные ресурсы, стоящие в очереди после нее, будут простаивать. Таким образом, если вы хотите выполнить сразу после загрузки страницы какой-то сценарий, то перед этим нужно задаться вопросом: "а все ли нужные для выполнения этой работы ресурсы уже загружены, или еще нет?" Уж сколько раз мне приносили сайты с плавающей ошибкой, когда javascript-код, предназначенный для модификации визуального оформления html-страницы, то работал, то нет, сосчитать тяжело. А вся беда в том, что javascript пытался изменить стилевое оформление той части страницы, которой еще нет (не успела она еще загрузиться в этот раз — вот в прошлый раз успела, а сейчас не смогла). На этой проблеме я уже многократно акцентировал внимание в своих статьях, посвященных javascript (событие onDomReady), так что оставим ее и посмотрим, что происходит после того, как сайт с ajax'ом загрузился, и пользователь пытается что-то на нем делать. В мире прикладного программирования есть такое понятие, как "application-транзакция" (бизнес-транзакция). Что такое транзакция вообще в программировании и в частности для СУБД, наверное, слышали многие. Вкратце: любая работа, которую выполняет приложение (и веб-приложение — не исключение), состоит из этапов. Этапы нужно выполнить в строгом порядке. Состояние системы до начала транзакции и после ее завершения должно быть согласованным с бизнес-правилами. Например, работает на нашей фирме сотрудник Вася или не работает — третьего не дано. А вот в ходе выполнения транзакции приложение может быть в "разобранном" (несогласованном) состоянии. Вот половину документов мы оформили, а половину еще не успели, но к концу рабочего дня все документы будут оформлены как это и требуется. Возможно, что в ходе выполнения работы произошел сбой (вот нашего сотрудника отдела кадров скрутил жесточайший насморк). Дальше оформлять документы он не может и уходит домой лечиться на неделю или две. А как быть с незавершенной транзакцией — читай приемом сотрудника на работу? Чтобы к нам не имели претензий проверяющие органы, мы должны транзакцию "откатить", т.е. кладем документы "под стекло" и просим Васю погулять эту недельку. Как вывод транзакция в мире СУБД гарантирует целостность данных в таблицах. Т.е. все действия, которые выполняются в рамках процедуры приема сотрудника на работу, оформления накладных, закупок товаров выполняются в рамках границ транзакции. И если в середине процедуры происходит сбой, то все предшествующие действия будут отменены, а база вернется в исходное, непротиворечивое, состояние. Теперь вернемся к понятию бизнес-транзакции и тому, чем она отличается от транзакции в БД. Редко, крайне редко серьезный бизнес в ходе процедуры, например, приема Васи на работу выполняет изменения только в одной БД. Чаще всего транзакции являются распределенными, и в ней участвуют несколько СУБД (наверняка территориально размещенных в разных местах). Более того, в транзакции могут участвовать другие приложения, устройства и конкретные люди (например, пойти поставить штамп на документ у директора). Так что, если наш директор забыл дома печать, то все предшествующие действия придется "откатывать". Для простоты положим, что бизнес-транзакция распространяется только на одну базу данных, и даже в этом случае нас ждут неприятности. Прежде всего, традиционные desktop-приложения отличаются от web-приложений тем, что в них отсутствует постоянное подключение к источнику данных. Грубо говоря, при запуске desktop приложение подключается к СУБД, получает номер соединения и "держит" его до своего закрытия. Таким образом, когда начинается процедура транзакции, СУБД как бы "делает" снимок актуального состояния БД. И в случае необходимости выполнить "откат транзакции" трудностей не возникает. Веб-приложения построены по другому принципу: каждый запрос клиента создает новое — подчеркиваю: новое — подключение к источнику данных. Когда работа скрипта завершается, то соединение "отдается" назад СУБД и теряется для нас безвозвратно.

Как вывод: все действия по модификации БД применительно к веб-приложениям нужно выполнить за один раз. Для примера: клиент должен заполнить некоторую форму, распределенную по нескольким страницам. Так, на первой странице он вводит сведения о себе, на второй — о своей семье, на третьей о — домашних животных. Мы не можем вносить сведения о заявке в СУБД до тех пор, пока не будет предоставлена вся информация. Мы ждем, пока не будет завершен ввод данных на третьей странице, а затем вносим сведения в БД. На словах все просто. Теперь задумаемся над тем, где сохранить промежуточную информацию? Как же, скажете вы: есть такое понятие, как сессия, и информация привязывается к ней. Все это хорошо, но есть две проблемы. Первая очевидная — проблема надежности. Серьезные (а мы говорим именно о них) приложения работают под большими нагрузками, и сбой может случиться в любую секунду. Когда это происходит, нельзя обойтись надписью на экране: "Упс, мы поломались, зайдите через пару часиков". Должна сработать система прозрачного для клиента переключения на запасной сервер, который как ни в чем не бывало продолжит обработку запроса (бизнес-транзакцию). В идеале обработка запросов должна выполняться набором серверов (кластером), которые плавно делят между собой нагрузку. И в случае сбоя одного из них выполняется переключение на другой сервер, который уже готов к работе. В частности, у него есть своя копия данных, которые были введены клиентом в сессию того сервера, который только что "поломался". Итак: первая проблема — это репликация (распространение) состояния клиента между несколькими серверами, обрабатывающими запросы. И хорошо если все "состояние" клиента кодируется парой строк текста, а если он в ходе бизнес-транзакции загружает на сервер многомегабайтные файлы? Вторая проблема связана с необходимостью консолидировать бизнес-правила в одном месте. К примеру, когда пользователь заполняет анкету для банка, то на первой странице он указывает свой возраст, затем начинает заполнять остальные страницы, и на последнем шаге (когда наше приложение пытается сохранить информацию в БД) узнает, что все напрасно, кредит ему не дадут, потому что он пенсионер. Собственно, это суровая правда жизни, но в чем суть проблемы для нас? В идеале мы должны вводимую информацию контролировать на каждом шаге. Т.е. заполнил пользователь поле со своим возрастом, а мы ему так ненавязчиво в углу странички вывели надпись: "увы, Василий Васильевич, но денег мы вам не дадим, и не надо дальше заполнять анкету". Как следствие бизнес-логика будет распределена по нескольким местам: проверки будет выполнять наш серверный код по мере прохождения процедуры заполнения анкеты. И еще одна дублирующая проверка будет выполнена самой СУБД при внесении информации в базу данных. Отказаться от проверки данных средствами СУБД невозможно — это приводит к стольким проблемам, что даже не стоит начинать разговора. Так что, когда бизнес-правила некоторой процедуры поменяются, то нам нужно синхронно и согласованно внести правки в проверяющий данные код, размещенный в нескольких местах.

Описанные выше проблемы кажутся совсем не проблемами, когда задумаемся над тем фактом, что с веб-приложением работают несколько пользователей одновременно. Вернее, работают не самим приложением, а данными, которые оно представляет им, и данные эти постоянно изменяются самими пользователями. К примеру, вы все знаете об mediawiki — замечательной платформе для создания сайтов, наполняемых информацией самими пользователями (например, сайт wikipedia). Рассмотрим сценарий, когда есть страница со статьей, например, об арбузах. Посетитель Вася прочитал статью и нашел в ней несколько ошибок, затем, движимый чувством любви к ближнему, он начинает исправлять статью. В этот же момент посетитель Петя, также движимый заботой о точности статьи, пытается ее отредактировать, исправить ошибки — другие ошибки — не те, которые исправляет Петя. Мы попадем в классическую ситуацию "гонок", когда два пользователя выполняют одну и ту же работу, и последний из них сохранив свои изменения, затрет правки, выполненные первым редактором. И затрет он их не потому, что информация Пети неправильная, а потому, что просто не знал о ней, не знал, что кто-то внес правки в документ, пока он его редактировал. Из описанной выше ситуации есть три выхода: "выигрывает последний" — этот вариант, когда теряются правки, выполненные первым редактором, совсем не хорош. Вариант два — "выигрывает первый" — характеризуется тем, что, когда первый редактор "забирает" статью для редактирования, то напротив нее в БД ставится специальная отметка (заблокирована для правки Васей). Так что, если Петя попробует открыть страницу и начать ее редактировать, то он получит сообщение об ошибке и будет вынужден ждать, пока Вася не завершит свою часть работы. Этот вариант не то чтобы плох, но и не слишком хорош. К примеру, Вася, заблокировав страницу, может отвлечься другими делами и забыть вернуть страницу в состояние "можно править всем". Можно было бы ввести понятие timeout'ов — промежутка времени, после которого блокировка документа автоматически теряется, но все же лучше попробовать третью стратегию: "последний думает за всех". В этом случае, когда Петя запрашивает документ на редактирование, то вместе с его содержимым Петя (незаметно для себя) получает номер или ревизию документа (можно дату его последней правки). Когда редактирование файла будет завершено, и данные отправляются обратно на сервер, он обнаружит, что номер ревизии, которую правил Петя, устарел (на сервере лежит файл с большим номером ревизии). А раз кто-то успел внести правки до Пети, то нужно отклонить операцию редактирования и вернуть Пете два документа: то, что было отредактировано Васей, и то, что было отредактировано самим Петей. Видя перед собой эти два файла (а лучше видеть еще третий файл с первоначальным состоянием документа), Петя должен собрать из этих двух правок одну общую и сохранить ее на сервер. В некоторых ситуациях подобный ход невозможен — например, изменения вносятся в двоичный файл (картинку) — как уж тут объединить правки? Также есть шанс, что Петя будет невнимателен и неаккуратно выполнит слияние документов. Хотя здесь больше вопрос о создании удобного интерфейса пользователя, сводящего к минимуму шанс не заметить что-либо. Написав эти строки, я заметил, что часть рассуждений повторяет мои же слова, когда я рассказывал о системах управления версиями документов (серия статей про svn), так что на этом я закруглюсь. Внимательный читатель, возможно, хочет сказать, что описанные мною проблемы являются общими для всех веб-приложений (также и для настольных приложений), а не только построенных на ajax. Все это верно, но ajax добавляет к описанным выше проблемам синхронизации действий нескольких пользователей еще проблему с синхронизацией действий самого клиента. В старые добрые времена (еще однозадачного DOS'а) пользователь работал с программой как? Нажал на кнопку — сиди и жди, пока результат не будет посчитан машиной и отображен на экране. А теперь можешь нажать на следующую кнопку, и снова сиди и жди. Сейчас же все не так: вот зашел наш пользователь на сайт и нажал на кнопку "показать последние новости", а пока данные грузятся ajax'ом, чтобы не скучать, жмет на кнопку "показать новые фотки ". Что-то загрузится первое, что-то попозже — не важно: ajax (асинхронный, одним словом). С одной стороны, это удобно, с другой — добавляет массу головной боли разработчиками. В описанном выше случае с загрузкой фотографий и новостей никаких сложностей нет: обе операции не зависят друг от друга, и действие с БД, которое они выполняют, — чтение. Как только одновременно можно делать несколько операций, работающих с одними и теми же данными (общим ресурсом на сервере) или вносящих изменения в БД, появляются сложности. Напоминаю, что ajax — значит, выполнение нескольких дел одновременно и в порядке, не совпадающем с тем, в каком они были инициированы пользователем. Так, хотя исходящий запрос на выполнение действия "A" ушел до отправки запроса "B", но на сервер они могут придти в ином порядке. Аналогично ответ сервера на операцию "A" может с равной долей вероятности придти до или после ответа на операцию "B". В случае фоток и новостей, как я говорил, перемешивание порядка действия большой роли не играло. А вот для серьезных приложений, изменяющих информацию, нам нужно заставить выполняться команды пользователей именно в том порядке, в котором они уходят на сервер. И если ответа на действие "A" еще не было получено, то отправлять на сервер действие "B" нельзя. Это так называемая синхронизация на стороне клиента. Есть еще вариант, когда упорядочение идет на стороне сервера (он сортирует приходящие запросы на выполнение работы в нужном порядке), но этот вариант сложен и применяется редко. Теперь задумаемся над тем, как технически организовать подобное упорядочение. В прошлой статье я рассказал о классе ConnectionManager из библиотеки YUI. Когда мы делаем с помощью его вызов, например:
r = YAHOO.util.Connect.asyncRequest('POST', ' >сайт callback, 'fio=vasya&age=12');

мы никак не можем указать, какое место в общей цепочке занимает данный вызов. Как вывод: от разбросанных где попало в коде вашего приложения вызовов YAHOO.util.Connect.asyncRequest придется избавляться, и как можно скорее. Это не означает, что придется отказываться от функций YUI и изобретать свой "велосипед". Просто все вызовы должны идти через единый объект — назовем его "Connector", — внутри которого должна быть создана очередь выполняемых запросов. Когда мы говорим:
Connector.callMyMethodOnServer (адрес скрипта, параметры, нужные для его работы)

то никакого вызова немедленного (подчеркиваю) обращения к серверу не должно выполняться. Вместо этого объект Connector должен поместить в свою внутреннюю очередь объект задания. Этот объект хранит сведения о том, какую операцию нужно выполнить, т.е. адрес запрашиваемой страницы и переменные, передаваемые серверному скрипту (те, кто читал что-то о паттернах проектирования, узнали паттерн Command, или Команда). Затем Connector должен каждые, скажем, 200 миллисекунд просматривать очередь заданий и в том случае, если (и только в этом случае) сейчас нет ни одного выполняемого запроса. То Connector может взять из очереди первый элемент и с помощью YUI-объекта ConnectionManager отправить запрос на сервер. Вполне вероятно может возникнуть ситуация, когда ответа от сервера не будет получено за приемлемый промежуток времени. В этом случае считается, что запрос был утерян, и Connector снова может брать задания на обработку из очереди. Все запросы, которые посылаются серверу, снабжаются уникальным числом (например, порядковым номером запроса). Это необходимо для того, чтобы избежать проблемы с "опоздавшими" запросами. К примеру, мы послали на сервер запрос №10 и не получили ответ в течение 5 секунд. Тогда мы посылаем следующий запрос — №11. Однако запрос №10 на самом деле дошел до сервера, просто с опозданием. Сервер же, видя, что пришел запрос 10 в то время, как он уже обработал запрос №11, игнорирует эту команду. И это правильно, т.к. мы (javascript-скрипт на стороне клиента) уже сообщили пользователю, что его команда была неудачна. Возможна, правда, и другая ситуация, когда сервер получил сначала запрос 10, потом 11, но на стадии отправки результатов клиенту запрос 10 слишком сильно опоздал. Тогда javascript-код, основываясь на нумерации запросов, его также игнорирует.

Что же, сегодняшняя статья в серии получилась более теоретической, чем раньше. В следующий раз я завершу рассказ об ajax и перейду к рассмотрению других модулей YUI.

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


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

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