Подписаться на рассылку сайта о всех новостях и изменениях через сервис рассылок.
Нам всем доводилось использовать технологию AJAX (Asynchronous Java
Technology and XML, Асинхронная технология Java и XML) и многие из нас открывают
здесь для себя целый новый мир.
В то время как многие программисты лишь случайно столкнутся с AJAX в рамках
существующих систем (так мы будем далее переводить термин framework), вы,
возможно, захотите исследовать эту технологию глубже или расширить уже имеющиеся
функциональности.
Этот FAQ предназначен для Java-разработчиков, которые хотели бы добавить
AJAX-функциональность в свои приложения.
Вне всякого сомнения, AJAX сейчас популярен, но, может быть, он не совсем
подходит именно вам: его использование ограничено web-браузерами самых последних
версий, а также заставляет решать вопросы совместимости web-браузеров и требует
новых знаний и навыков. Поэтому прежде чем вы с головой погрузитесь в AJAX, вам
стоит прочитать очень хороший блог (blog) Алекса Босворта (Alex Bosworth) на
странице
AJAX Mistakes.
С другой стороны, AJAX позволяет создавать web-приложения с богатыми
интерактивными возможностями, которые реагируют на действия пользователя и
выполняются
действительно быстро.
Хотя утверждение о том, что приложения, использующие AJAX, работают быстрее,
может показаться спорным, нельзя отрицать тот факт, что пользователи испытывают
чувство незамедлительной реакции приложения, так как AJAX обеспечивает для них
активную обратную связь в то время, как обновление данных происходит в фоновом
режиме "за кадром".
Если вы не боитесь решать вопросы совместимости web-браузеров и готовы к
получению новых навыков, AJAX - именно то, что вам нужно.
Для начала попробуйте изменить с помощью AJAX лишь небольшие части или
компоненты ваших приложений. Нам всем нравятся новые технологии, однако не стоит
забывать, что применение AJAX должно улучшать взаимодействие с пользователем, а
не быть ему помехой.
Конечно. Java прекрасно работает вместе с AJAX! Вы можете использовать
JavaEE-серверы (Java Enterprise Edition), чтобы создавать AJAX-страницы для
клиентских web-приложений и обслуживать поступающие от них AJAX-запросы,
управлять на стороне сервера состоянием объектов, связанных с клиентскими
приложениями, использующими AJAX, и предоставлять AJAX-клиентам различные
серверные ресурсы.
Компонентная модель
JavaServer Faces отлично подходит для определения и использования
AJAX-компонентов.
Вполне вероятно, что вы уже пользуетесь преимуществами AJAX: многие
существующие системы на основе Java обеспечивают определенное AJAX
взаимодействие, а новые системы и библиотеки компонентов создаются с улучшенной
поддержкой AJAX.
Я не стану приводить здесь список всех систем, базирующихся на Java и
обеспечивающих работу с AJAX, из опасения что-то пропустить. Хороший список
приводится на сайте
www.ajaxpatterns.org/Java_Ajax_Frameworks.
Если вы еще не выбрали подходящую систему, я бы рекомендовал вам рассмотреть использование технологии JavaServer Faces (JSF) и инструментарий на базе этой технологии. Создание и использование JSF-компонентов позволяет абстрагироваться от рассмотрения многих деталей генерации JavaScript-кода, AJAX-взаимодействия клиента и сервера, обработки DHTML-данных и, таким образом, упростить применение технологии AJAX как разработчиками JSF-приложений, так и плагинами в JSF-совместимых интегрированных средах разработки (Integrated Development Environment, IDE), таких как Sun Java Studio Creator.
Если система, которую вы используете, не вполне подходит для решения ваших задач и вы хотите разработать свои собственные AJAX-компоненты или дополнительную функциональность, начните со статьи Asynchronous JavaScript Technology and XML (AJAX) With Java 2 Platform, Enterprise Edition.
Самый простой пример с исходным кодом приводится в статье Using AJAX with Java Technology. Более полный список AJAX-ресурсов приводится на странице Blueprints AJAX Home.
Полезно изучить статью AJAX libraries and frameworks, чтобы не изобретать велосипед при разработке собственных клиентских AJAX-скриптов.
Книга AJAX in Action, написанная Дейвом Крейном (Dave Crane) и Эриком Паскарело (Eric Pascarello) совместно с Дарреном Джеймсом (Darren James) полезна Java-разработчику, поскольку содержит готовые примеры JavaScript.
Если вы не планируете пользоваться уже готовыми AJAX-компонентами, вам следует изучить следующие технологии.
Dynamic HTML (DHTML) - технология, которая является основой для AJAX. DHTML обеспечивает взаимодействие пользователя с приложением в режиме реального времени средствами браузера. DHTML - сочетает в себе JavaScript, Документную Объектную Модель (Document Object Model, DOM) и Каскадные Таблицы Стилей (Cascading Style Sheets, CSS).
Также важно понимать, что сутью HTTP протокола является механизм запросов/ответов (request/response). Многие трудноуловимые ошибки могут возникнуть, если вы игнорируете различия между методами GET и POST при настройке клиентского объекта XMLHttpRequest или при обработке кодов HTTP-ответов во время обратных вызовов.
JavaScript соединяет воедино все остальные части клиентского интерфейса. Во-первых, он используется для создания объекта XMLHttpRequest и запуска асинхронных вызовов. Во-вторых, JavaScript позволяет анализировать и обрабатывать возвращаемые с сервера данные и сообщения. И наконец, JavaScript позволяет добавлять новое содержание в HTML документ и изменять CSS стили, используя DOM API.
С одной стороны да, если вы планируете разрабатывать новую функциональность с использованием AJAX в своих web-приложениях.
С другой стороны, JSF-компоненты (JSF components) и их библиотеки могут скрывать детали, касающиеся JavaScript, DOM и CSS. Эти компоненты могут сгенерировать для вас все необходимые составные части для обеспечения поддержки взаимодействий с использованием AJAX. Визуальные средства разработки, такие как Java Studio Creator, также позволяют применять JSF-компоненты c поддержкой AJAX для создания эффективных web-приложений, позволяя при этом разработчикам не заботиться о специфичных деталях реализации AJAX.
Если вы планируете разрабатывать свои собственные JSF-компоненты или
обрабатывать генерируемые ими события, важно, чтобы вы были знакомы с основами
JavaScript. Существуют клиентские
JavaScript-библиотеки (смотри ниже), которые можно вызывать из JavaScript на
своей странице и которые автоматически решают проблему несовместимости
браузеров.
Кроме этого существует интересный Internet-ресурс, знакомящий Java-разработчиков
с объектами JavaScript:
Иерархия
объектов и наследование в JavaScript (Object Hierarchy and Inheritance in
JavaScript).
Существует множество JavaScript-библиотек и наборов компонентов (еще большее количество появляется новых), которые помогают справляться с такими малоприятными проблемами, как различия браузеров. Рекомендуется обратить внимание на 3 хорошие библиотеки - The Dojo Toolkit, Prototype и DWR.
Существует множество устаревших библиотек и совершенно новых библиотек для JavaScript, но вышеприведенный список дает обзор только нескольких неспециализированных библиотек. Вы можете выбрать как одну библиотеку, так и использовать несколько в соответствии с вашими потребностями. Вот более широкий список клиентских систем: Survey of AJAX/JavaScript Libraries.
Понятно, что символ 'X' в слове "AJAX" означает "XML", но поклонники AJAX быстро обратили внимание, что, по существу, ничто в AJAX не мешает использовать другие типы выходных наборов данных: JavaScript, HTML или plain text.
eval(), вы можете затем создать этот объект
на стороне клиента.
JSON, который представляет собой спецификацию по обмену данными на
основе JavaScript-объектов, полагается именно на такую технику. Mashup - это популярный термин, обозначающий создание совершенно нового web-приложения путем объединения данных от различных web-сервисов и другого online ПО. Хороший пример mashup - housingmaps.com, который наглядно объединяет объявления по жилью с craiglist.org и географические карты с maps.google.com.
В результате использования техники динамического обновления web-страницы с
помощью данных, полученных через AJAX-взаимодействие и DHTML, внешний вид
страницы и ее структура могут сильно измениться.
Пользователь может захотеть в любое время нажать кнопки браузера "Back" или
"Forward", создать закладку для страницы, скопировать URL страницы из строки URL
браузера и поделиться ею с другом через e-mail или чат, распечатать страницу.
Поэтому при проектировании AJAX-приложений вам необходимо все тщательно
продумать, чтобы ожидаемое поведение при навигации, создании закладок, печати и
прочих действий в браузере было бы таким, как описано ниже.
Другие соображения, полезные для разработчиков, использующих AJAX:
Degradability - это термин, используемый для описания технических приемов, предназначенных для адаптирования приложений к широкому диапазону web-браузеров. Многие AJAX-библиотеки имеют встроенную degradability. Но если вы будете создавать свою собственную AJAX-функциональность, просто следуйте практическим рекомендациям, предоставляемым организациями по выработке стандартов, такими как World Wide Web Conrsoritum (W3C), продвижению стандартов, такими как Web Standards-сообщество, и многими другими. В этом случае ваше приложение может работать эффективно в браузерах, которые не поддерживают AJAX, т.к. даже утратив часть своих эффектных возможностей при выполнении в таких менее "способных" браузерах, ваше приложение все равно будет готово к использованию.
Помните, что не стоит применять AJAX только ради демонстрации собственной "крутизны". Мотив для создания приложения - чтобы люди его использовали. Но люди не станут пользоваться вашим приложением, если оно несовместимо с их web-браузером.
Существует не очень много инструментальных средств, которые бы поддерживали отладку как клиентской, так и серверной частей. Я уверен, что эта ситуация будет меняться по мере роста числа AJAX-приложений. В настоящее время я выполняю отладку клиентской и серверной частей приложения раздельно. Ниже приводится информация об отладке клиентской части приложения для некоторых наиболее популярных браузеров.
При использовании отладчиков может также применяться и знание общих
технических приемов, как например отладочный вывод ("Alert Debugging"). Вы
просто помещаете вызов функции "alert()" внутри кода JavaScript подобно вызову
System.out.println() в java-коде. Этот маленький фрагмент годится для
большинства случаев.
Некоторые системы, такие как
Dojo,
предоставляют API для трассировки отлаживаемых операторов.
HTTP метод GET следует использовать для получения данных, возвращаемых по
URL-запросу, параметры которого не будут меняться. Если же состояние обновляется
на сервере, следует использовать HTTP-метод GET. Если же данные клиентского
AJAX-запроса будут обновляться на сервере, должен использоваться HTTP-метод
POST.
Эти правила согласуются с
HTTP
idempotency recommendations. Их настоятельно рекомендуется соблюдать для
сохранения непротиворечивости архитектуры web-приложения.
Тот факт, что в AJAX-запросах используется XML, вовсе не означает, что вы сможете без дополнительных настроек правильно посылать и принимать локализованные данные. Для создания локализованных AJAX-компонентов, вам надо сделать следующее:
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
escape(), которая
возвращает escape-последовательность (текстовую строку) в кодировке Unicode,
где все национальные символы заменены своим шестнадцатиричным
представлением. Более подробно см.:
Сравнение
escape(), encodeURI(), and encodeURIComponent().
HttpServletRequest.setCharacterEncoding() перед тем, как вы будете
извлекать локализованный параметр с помощью вызова
HttpServletRequest.getParameter(). В случае UTF это будет выглядеть
так: request.setCharactherEncoding("UTF-8");.
В серверном компоненте при генерации AJAX-компонентов, необходимо задавать ту же самую кодировку, что и для web-страницы:
response.setContentType("text/xml;charset=;UTF-8");
response.getWriter().write("<response>invalid</response>");
Для дополнительной информации по применению AJAX с JEE технологиями см.: AJAX and Internationalization, а для разработки многоязычных приложений: Developing Multilingual Web Applications Using JavaServer Pages Technology.
Используя JavaScript, можно одновременно обрабатывать несколько
AJAX-запросов. Чтобы гарантировать правильный порядок их последующей обработки,
рекомендуется использовать технику
JavaScript Closures.
Пример ниже показывает использование XMLHttpRequest-объекта, скрытого внутри
другого JavaScript-объекта с именем AJAXInteraction. В качестве
аргументов функции AJAXInteraction() вы передаете URL, на который
посылается HTTP-запрос, и функцию (callback), которая вызывается после обработки
запроса сервером.
function AJAXInteraction(url, callback) {
var req = init();
req.onreadystatechange = processRequest;
function init() {
if (window.XMLHttpRequest) {
return new XMLHttpRequest();
} else if (window.ActiveXObject) {
return new ActiveXObject("Microsoft.XMLHTTP");
}
}
function processRequest() {
if (req.readyState == 4) {
if (req.status == 200) {
if (callback) callback(req.responseXML);
}
}
}
this.doGet = function() {
req.open("GET", url, true);
req.send(null);
}
this.doPost = function(body) {
req.open("POST", url, true);
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
req.send(body);
}
}
function makeRequest() {
var ai = new AJAXInteraction("processme", function() { alert("Doing Post Process");});
ai.doGet();
}
Функция makeRequest() в приведенном примере создает объект
AJAXInteraction с 2 параметрами: URL со значением "processme" и
inline-функцию, которая будет показывать Alert-диалог с сообщением "Doing Post
Process". Когда выполняется вызов ai.doGet(), инициируется
AJAX-взаимодействие и, когда серверный компонент, связанный с URL "processme",
возвратит документ, тот передается callback-функции, которая была определена при
создании AJAXInteraction.
Применение техники " closures" гарантирует, что будет вызываться именно та callback-функции, которая связана с соответствующим AJAX-взаимодействием. Однако следует проявлять осторожность при создании большого числа closure-объектов в том смысле, что создание новых объектов XmlHttpRequests (как это сделано в примере) будет ограничено числом сокетов (sockets), используемых для передачи запросов в данный момент времени. Это происходит вследствие ограничения на число запросов, которые могут выполняться параллельно. Например, Internet Explorer разрешает только 2 одновременных AJAX-запроса в данный момент времени. Другие браузеры могут разрешать большее число запросов, но обычно от 3 до 5. Вы также можете использовать пул AJAXInteraction-объектов.
Следует отметить, что обычно, когда клиент выполняет несколько AJAX-вызовов, аналогичный порядок возврата ответов от сервера не гарантирован. Применение описанной выше техники в callback-функции closure-объекта гарантирует правильную последовательность обработки.
Много полезного можно почерпнуть из обсуждения, озаглавленного Ajaxian Fire and Forget Pattern.
Заголовку "Content-Type" должно быть присвоено значение "text/xml". В
сервлете это можно сделать с помощью метода
HttpServletResponse.setContentType() - надо передать ему значение
"text/xml", когда возвращаемые данные имеют тип XML. Однако многие реализации
XMLHttpRequest дадут ошибку, если будет указан заголовок "Content-Type".
Фрагмент кода ниже показывает, как задать значение для "Content-Type":
response.setContentType("text/xml");
response.getWriter().write("<response>invalid</response>");
Вы можете также задать значение заголовка "Cache-Control", например, при использовании автозаполнения, чтобы дать понять proxy-серверу и браузеру не кэшировать полученные результаты:
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.getWriter().write("<response>invalid</response>");
Замечание для разработчиков: Internet Explorer автоматически будет использовать кэшированные результаты AJAX-ответов на HTTP-запросы по методу GET, если данный заголовок не задан, что может представлять трудность для разработчиков. При разработке это следует учитывать и присваивать значение этому заголовку.
Как и в случае других web-приложений, выполняемых в браузере, у вас есть несколько вариантов:
Поскольку в последнее время обработка данных и управление ими все более смещаются на сторону клиента, возможно, потребуется пересмотреть варианты хранения состояния AJAX-клиента.
При создании формы следует в элементе "form" назначить атрибуту
"onSubmit" имя соответствующей JavaScript-функции, которая
возвращает значение false:
<form onSubmit="doAJAXSubmit();return false;" > <input type="text" id="tf1" /> <input type="submit" id="submit1" value="Update"/> </form>
Также для отправки формы можно использовать кнопку (input-элемент
"button" в элементе "form"), назначив ей аналогичным
образом соответствующую JavaScript-функцию:
<form onSubmit="doAJAXSubmit();return false;" > <input type="text" id="tf1" /> <input type="button" id="button1" onClick="doAJAXSubmit()" value="Update"/> </form>Обратите внимание, что значение атрибута формы
"onSubmit" задано и
в этом примере. Это сделано для того, чтобы корректно обрабатывать ситуации,
когда пользователь, например, во время редактирования текстового поля нажмет
клавишу "Enter" и форма будет отправлена.
При обновлении страницы рекомендуется дождаться, пока AJAX-вызов успешно обновит данные формы, а потом обновлять данные на странице. В противном случае обновление может выполниться некорректно, но пользователь не будет об этом знать. Я обычно во время частичного обновления данных вывожу информативное сообщение, а после успешного завершения AJAX-взаимодействия обновляю web-страницу.
Управление приложением может быть сосредоточено у серверных компонентов, а может быть распределено между клиентом и сервером. Это надо анализировать и выбирать наиболее подходящий вариант. Для AJAX более выгодно распределение управленческих функций между клиентом и сервером.
Встречаются случаи, когда все AJAX-приложение может полностью состоять только из одной страницы. При этом следует помнить, что если вы выбираете этот тип управления, необходимо тщательно продумать вопросы навигации и создания закладок.
Выбор конкретного варианта управления зависит от целей, которые вы хотите достигнуть. Я предпочитаю распределять управление между клиентом и сервером.
Если пользователь выберет режим просмотра исходного текста HTML-страницы
(view page source), он сможет увидеть все содержащиеся на этой странице
JavaScript-сценарии в виде обычного текста.
Но JavaScript по умолчанию не имеет доступа к локальной файловой системе, если
пользователь специально не предоставит такие права. AJAX-взаимодействие может
осуществляться только с теми серверными компонентами, которые содержатся на том
же сервере, откуда была загружена текущая web-страница. Для AJAX-взаимодействий
с внешними сервисами следует использовать proxy-шаблоны.
Вам следует соблюдать осторожность и не раскрывать модель приложения таким образом, чтобы у недобросовестного пользователя появилась возможность получения доступа к исходным кодам или декомпиляции ваших серверных компонентов.< В случае обмена конфиденциальной информацией, стоит подумать над использованием протокола HTTPS для создания защищенного соединения.
В аббревиатуре AJAX не зря присутствует слово "асинхронный". Синхронный запрос заблокирует обработку любых событий на странице до получения ответа от сервера (по сути, в этом случае браузер перестанет реагировать на любые действия пользователя до завершения синхронного запроса). И я практически не представляю, в каких случаях синхронный запрос был бы предпочтительнее асинхронного.
Не торопитесь отказываться от тех частей вашего приложения, которые используют плагины или аплеты. Хотя AJAX и DHTML могут выполнять "drag and drop" и другие действия с GUI, все же для этих технологий существуют ограничения, особенно когда это касается поддержки браузеров. Аплеты и плагины существуют уже давно и они давно могут выполнять запросы аналогично AJAX. Аплеты содержат большой набор компонентов GUI и имеют API, который предоставляет разработчикам буквально все.
Многие игнорируют аплеты и плагины, т.к. для их начальной инициализации
необходимо время при запуске приложения, а также нет гарантии, что у клиента
установлена нужная для работы аплета или плагина версия JVM. Кроме того, аплеты
и плагины не всегда способны манипулировать объектами DOM на странице.
Однако если ваше приложение всегда будет работать в одинаковом окружении или вы
можете позволить себе зависимость от конкретной версии< JVM или доступной версии
плагина (как в случае корпоративного окружения), решение на основе
аплетов/плагинов вполне обоснованно.
Один момент, на который стоит обратить внимание - совместное использование
AJAX и аплетов/plugins. Например, Flickr
использует комбинацию AJAX/DHTML-взаимодействий для маркировки картинок и plugin
для манипуляций отдельными фотографиями и их наборами.
Если вы разрабатываете свои собственные серверные компоненты, было бы хорошо,
чтобы они могли работать с обоими типами клиентов (AJAX/DHTML и аплет/plugin).
Конечно, вы можете создавать собственное решение для отслеживания текущего состояния вашего приложения, но я рекомендую оставить это экспертам. Dojo позволяет выполнить навигацию независимо от браузера так, как показано в примере ниже:
function updateOnServer(oldId, oldValue, itemId, itemValue) {
var bindArgs = {
url: "faces/ajax-dlabel-update",
method: "post",
content: {"component-id": itemId, "component-value": itemValue},
mimetype: "text/xml",
load: function(type, data) {
processUpdateResponse(data);
},
backButton: function() {
alert("old itemid was " + oldId);
},
forwardButton: function(){
alert("forward we must go!");
}
};
dojo.io.bind(bindArgs);
}
Приведенный пример будет обновлять значение на сервере с помощью вызова dojo.io.bind(). Обратите внимание, что свойствам, отвечающим за обработку событий от кнопок браузера "Back" и "Forward", присвоены соответствующие функции. Вы можете восстановить значение в oldValue или выполнить другие нужные действия. Все детали реализации того, как обнаруживается событие от нажатия соответствующей кнопки браузера, скрыто от разработчика внутри библиотеки Dojo.
Ссылка AJAX: How to Handle Bookmarks and Back Buttons содержит подробности по данной проблеме и предлагает JavaScript-библиотеку Really Simple History framework (RSH), специализирующуюся только на вопросах back- и forward-навигации.
В некоторых приложениях, таких как Google Maps вам может показаться, что, в процессе AJAX-взаимодействия пересылаются изображения. В действительности происходит следующее: в качестве ответа на AJAX-запрос пересылаются URL изображений и изображения, находящиеся по этим URL, затем с помощью DHTML вставляются в документ.
В примере, приведенном ниже, в результате AJAX-взаимодействия возвращается XML-документ и на основании его данных заполняется таблица категорий.
<categories>
<category>
<cat-id>1</cat-id>
<name>Books</name>
<description>Fun to read</description>
<image-url>books_icon.gif</image-url>
</category>
<category>
<cat-id>2</cat-id>
<name>Electronics</name>
<description>Must have gadgets</description>
<image-url>electronics.gif</image-url>
</category>
</categories>
Обратите внимание, что элемент image-url содержит URL
(местоположение) графического файла, представляющего данную категорию.
Callback-метод данного AJAX-взаимодействия выполнит анализ полученного
XML-документа и вызовет функцию addCategory() для каждой категории,
присутствующей в этом XML-документе. Функция addCategory() ищет в
теле документа таблицу "categoryTable" и добавляет в нее строку с
элементом-изображением.
<script type="text/javascript" >
...
function addCategory(id, name, imageSrc) {
var categoryTable = document.getElementById("categoryTable");
var row = document.createElement("tr");
var catCell = document.createElement("td");
var img = document.createElement("img");
img.src = ("images\" + imageSrc);
var link = document.createElement("a");
link.className ="category";
link.appendChild(document.createTextNode(name));
link.setAttribute("onclick", "catalog?command=category&catid=" + id);
catCell.appendChild(img);
catCell.appendChild(link);
row.appendChild(catCell);
categoryTable.appendChild(row);
}
</script>
...
<table>
<tr>
<td width="300" bgoclor="lightGray">
<table id="categoryTable" border="0" cellpadding="0"></table>
</td>
<td id="body" width="100%">Body Here</td>
</tr>
</table>
Обратите внимание, что атрибуту src элемента img
задается полный путь к файлу изображения: "images\" + imageSrc.
Само изображение будет загружено после того, как строка с указанным img-элементом
будет добавлена в таблицу categoryTable. В результате
последующего HTTP-запроса будет загружен графический файл с URL "images/books_icon.gif"
или "images/electronic_icon.gif"
JavaScript в отличие от Java не имеет встроенной поддержки работы с потоками.
JavaScript функции вызываются, когда происходит некоторое событие на странице,
например, загрузка страницы, щелчок мыши, получение фокуса элементом формы. Но
вы можете использовать таймер с помощью вызова setTimeout(),
передав в качестве аргументов имя функции, с которой этот таймер связан, и время
задержки в миллисекундах. Затем вы можете организовать цикл, вызывая ту же самую
функцию, как это показано в примере ниже:
function checkForMessage() {
// начать AJAX-взаимодействие с использованием processCallback() в качестве callback-функции
}
// callback для запроса
function processCallback() {
// выполнить действия после обработки запроса
setTimeout("checkForMessage()", 10000);
}
Заметьте, что в приведенном примере циклический вызов функции
checkForMessage() будет продолжаться бесконечно.
Можно изменять интервал таймера в зависимости от активности на странице или иных
условий. Кроме того, можно добавить логическое условие, чтобы прерывать цикл,
например, по результатам обработки AJAX-ответа.
Последние комментарии