AJAX: его история, устройство и проблематика
Наверное, все, кто так или иначе имеет дело с веб-разработкой, слышали и знают аббревиатуру AJAX. О ней много вопросов на собеседованиях, снято много обучающих роликов, написана куча разных how-to по JavaScript. Об AJAX говорят как о какой-то прорывной технологии, без которой ваш сайт — это унылый динозавр. Но… скорее, это AJAX — унылый распиаренный динозавр. Давайте попробуем понять, почему, в этой статье для всех фронтендеров-новичков.
Содержание:
1. История вопроса
2. Немного практики
3. Надо делать POST!
4. Мне бы чего-нибудь попроще, пожалуйста
5. Что еще может быть под капотом?
1. История вопроса
Итак, приступим. Первое, что надо знать о всемирной паутине — все это исторически сложившиеся обстоятельства. Так уж получилось, что самые первые веб-сайты задумывались с возможностью «путешествовать» по линкам гиперссылкам, и, чтобы выделить линки в тексте а заодно и другие красивости форматированного текста, придумали HTML Hyper Text Markup Language. Чтобы было проще передавать тексты в HTML, придумали HTTP Hypertext Transport Protocol, а в браузеры встроили модули, умеющие создавать HTTP-запросы.
Если опустить детали, основная особенность такого модуля — это способность понимать и передавать разные типы данных и на лету трансформировать принимаемый HTML в дерево объектов DOM Document Object Model. Каждый тег в тексте HTML — это нода в DOM. Сервер выдает строку, а на выходе модуля в браузере — объект, готовый к рендерингу на экране. Удобно!
Неудобно только то, что каждый запрос документа из браузера требует от сервера передать полную структуру страницы, даже если какие-то ее части уже получены. Логично предположить, что было бы неплохо найти способы получать только изменившиеся данные, не передавая повторно уже имеющиеся. И такие способы безусловно были предложены сообществом разработчиков во множестве альтернатив. Но не все прижились.
Например, модель Comet. Если вы вдруг вспомнили о чистящих средствах, то не напрасно. 😉 Оба бренда уже существовали на тот момент и вели такую же эпичную маркетинговую битву как в магазинах, так и в рекламе на экранах телевизора.
Конечно, получение кусочков веб-контента никак не связано с отбеливателями, но если из составных частей можно сложить звучную и раскрученную аббревиатуру, то почему бы и нет? Так появился AJAX — Asynchronous JavaScript and XML. Согласитесь, что звучит лучше, чем более логичное AJSD — Asynchronous JavaScript and Data.
Но новая ли это «технология»?
Принцип — да, новый, если сравнивать с необходимостью перегружать страницу. Но если помнить, что браузеры изначально имеют модуль, способный создавать HTTP-запросы и выдавать форматированные данные будь то HTML, XML, изображение или другое, то стоит просто обернуть этот модуль в публичный объект, добавить элементов управления и прилепить коллбэки — и вот вам готовый «новый» инструмент! Особо и переделывать ничего не надо.
Этим AJAX и прижился: просто имплементировать в браузер, не надо переделывать серверы — получай данные асинхронно и делай с ними что хочешь. Только надо помнить — это все те же стандартные HTTP-запросы, у которых размер заголовков бесполезной для страницы информации может многократно превышать размер нужных данных, плюс постоянно есть необходимость перекодировать данные в строку (а на сервере декодировать)… Динозавр, другими словами, но простой и отлаженный, а потому популярный. И хотя способов получить данные, не перегружая страницу, много, подход AJAX — это именно о том, чтобы асинхронно получить данные через старый добрый XMLHttpRequest
.
В конце я вернусь к альтернативам, а пока давайте рассмотрим динозавра с практической точки зрения.
2. Немного практики
Тут надо сделать ремарку, что «практических точек зрения» всегда несколько и они зависят от уровня решаемой задачи. Если брать самый верхний уровень — презентации страницы, — то разработчику надо выполнить как минимум следующие действия:
- определить часть страницы, которая зависит от внешних данных;
- описать, с какими данными и как эта часть страницы будет связана… и все.
В целом, этого достаточно для понимания, что именно и когда надо перерисовывать на странице.
«И где тут AJAX?», — спросите вы.
А нигде! Для этого уровня разработки вообще не важно, как вы связываете данные и DOM. Чтобы столкнуться с AJAX, надо спуститься минимум на уровень ниже, где вы определяете компоненты страницы.
Тут задач уже побольше:
- определить момент, когда данные устарели;
- выдать запрос серверу (AJAX);
- получить данные от сервера (AJAX);
- проверить/обработать полученные данные (+/- AJAX);
- обработать состояния ошибок (+/- AJAX);
- выполнить действия со страницей в соответствии с данными.
Причем задачи 2, 5 и 6 выполняются по-разному в разных браузерах. Но раз уж мы рассматриваем только AJAX, давайте посмотрим, насколько сложно реализовать связанные с ним подпункты.
Итак, сначала нам надо определить, какой запрос посылать серверу. Поскольку это всегда HTTP-запрос, то подходящие виды ограничиваются теми, которые предоставляет этот стандарт и которые могут возвращать данные. Это GET и POST.
Потом надо определиться с тем, что конкретно от клиента (браузера) ожидает сервер, чтобы передать нужные данные в запросе. Их можно передать и через GET, и через POST, но с разными ограничениями и требованиями к передаваемому формату. Например, данные можно поместить прямо в ссылку, как ее параметры:
https://www.example.com/page?param=value
То есть для того, чтобы передать данные, вам сначала надо представить их как объект, потом преобразовать в пары «ключ=значение
», преобразовать запрещенные в ссылках символы в эскейп-последовательности (о чем многие часто забывают), а потом все это конкатенировать в строку, которую надо присоединить к ссылке. Операция несложная, но рутинная. Благо, в браузерах для этого есть уже готовые инструменты.
Скажем, нам надо передать по ссылке https://www.example.com/page такой объект:
{ parameter: 'значение' }
последовательность действий должна быть приблизительно такой:
const myUrl = new URL('/page', 'https://www.example.com', ); myUrl.searchParams.append('parameter', 'значение'); // повторить для каждой пары ключ-значение
если проинспектировать созданный объект myUrl
, то можно заметить, что утилита сразу же перекодировала кириллицу в эскейп-последовательности:
URL { ... search: "?parameter=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5" }
Строка стала ̶н̶е̶о̶ж̶и̶д̶а̶н̶н̶о̶ намного длиннее, кодируя unicode
-символы.
Поэтому тут надо подумать о конечном размере строки ссылки, даже если вам кажется, что все поместится. Разные браузеры и веб-серверы имеют разные ограничения по максимальной длине ссылки, потому лучше ориентироваться на минимальный из существующих — 2048 байт.
Но можно установить и другое значение, которое будет работать в вашей системе, назовем ее константой MAX_URL_SIZE
. Тогда проверка должна быть как минимум такой:
if (myUrl.href.length > MAX_URL_SIZE) throw new Error( … )
Ну и, наконец, можно делать сам запрос… или нет, вспомнив о том, что у Internet Explorer нет window.XMLHttpRequest
, а есть window.ActiveXObject
. Но поскольку это уже история, то волочить код для совместимости с Internet Explorer ниже седьмой версии смысла в принципе не имеет. Потому:
const xhr = new XMLHttpRequest(); // константа, чтобы избежать случайного переопределения, что сохранит много нервов при дебаге асинхронных процессов xhr.onreadystatechange = function(){ // тут мы обрабатываем ответ сервера }; xhr.open('GET', myUrl.href, true); xhr.send();
Обязательно сначала .open()
с третьим параметром true
делает запрос асинхронным, потом .send()!
Вроде бы справились. Но… это еще не все.
Ошибки, ошибки — всегда ожидайте ошибки. Всемирная паутина может «порваться» в самый неожиданный момент. А также не забывайте, что запрос-то асинхронный и имеет несколько состояний, и чтобы получить данные, нам надо дождаться состояния окончания процесса. Состояния описывает свойство:
XMLHttpRequest.readyState
А чтобы не забивать голову справочной информацией о том, что код состояния завершения запроса равен 4, лучше использовать встроенные в объект поля-константы. В нашем случае это XMLHttpRequest.DONE
.
Плюс, сам сервер может вместо данных прислать ошибку, потому надо также проверять свойство
XMLHttpRequest.status
куда всегда записывается код ошибки. С учетом всего сказанного, наша функция обработки ответа сервера должна выглядеть где-то так:
xhr.onreadystatechange = function(){ if (this.readyState === XMLHttpRequest.DONE) { if (this.status === 200) { doSomeUseful(this.responseText); } else { // тут обработать ошибку } } };
Можно, конечно, положиться на особенность XMLHttpRequest
генерировать события типа ProgressEvent на каждое изменение своего состояния и обрабатывать их как заблагорассудится:
xhr.addEventListener('loadstart', eventHandler); xhr.addEventListener('load', eventHandler); xhr.addEventListener('loadend', eventHandler); xhr.addEventListener('progress', eventHandler); xhr.addEventListener('error', eventHandler); xhr.addEventListener('abort', eventHandler);
Тогда состоянию XMLHttpRequest.DONE
будет соответствовать событие ‘load
‘, а если сервер отвечает кодом, отличным от 200, то будет вызвано событие ‘error
‘. А функция обработки событий может быть такой:
function eventHandler(e) { switch (e.type) { case 'load': doSomeUseful(e.target.responseText); break; case 'error': // тут обработать ошибку break; default: // все остальное } }
Осталось только разобраться, что же за данные нам вернул сервер. Для этого есть свойство
XMLHttpRequest.responseType
и вот тут снова можно вспомнить историю, обнаружив среди типов данных — document
, а в объекте запроса свойство
XMLHttpRequest.responseXML
которое в ответе за “Х” в AJAX. Чем оно занимается? — Пытается преобразовать полученный текст в дерево объектов, если тип данных в заголовке запроса установлен как document
. Для этого текст должен быть в формате XML/HTML, и если парсинг прошел успешно, то по этому свойству находится объект с данными, а если нет, то null
. Насколько оно востребовано сейчас?
Честно — я ни разу не пользовался (зачем пользоваться динозаврами?), поскольку для передачи данных есть более оптимальные форматы.
А что делать, если надо передать более 2k данных на сервер?
3. Надо делать POST!
Все то же самое, только теперь надо иметь ввиду, что в отправляемом запросе появляется body
с данными, а значит в заголовке требуется указать их MIME-тип, чтобы сервер их правильно разобрал. Например:
const xhr = new XMLHttpRequest(); const param = 'длиииинное значение'; xhr.open('POST', 'https://www.example.com/page', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('param=' + encodeURIComponent(param));
и при необходимости, конечно, добавить функцию отслеживания состояния запроса и получения данных, если они ожидаются в принципе, как это выше делалось для GET
.
Если вы заметили, то тут появилась новая функция из встроенного в браузер инструментария — encodeURIComponent
. Ее надо обязательно использовать на отправляемых данных, если, конечно, не хотите постоянно ловить сообщения об ошибках. В примере с GET
мы ее не использовали, потому что она вызывается автоматически внутри методов класса URL.
Вот и вся «технология».
4. Мне бы чего-нибудь попроще, пожалуйста
Ну как вам AJAX? Удобная штука?
А вот так, например, удобнее будет?:
$.post( 'https://www.example.com/page', { param: 'длиииинное значение' }) .done(data => { // данные получены });
Это все тот же XMLHttpRequest
, только завернутый в удобный промис от JQuery. Вам не надо думать о заголовках, последовательности вызова методов, отслеживании состояний…
Получается намного проще! Настолько проще и удобнее, что подобный подход был внесен в стандартные Web APIs как Fetch API. Теперь в браузерах последних лет нет необходимости подгружать сторонние библиотеки, чтобы не спускаться на уровень непосредственной работы с XMLHttpRequest
.
А как вам вообще не думать об AJAX, просто поставив атрибут связи с данными в HTML тег? Например, как это делается в amp:
<amp-list src="/static/inline-examples/data/amp-list-urls.json"> <template type="amp-mustache"> <a href="{{url}}">{{title}}</a> </template> </amp-list>
Создатели компоненты заранее подумали о всех пунктах, описанных выше, от вас лишь требуется указать источник данных и знать их структуру. Красота! Но ничего нового в коммуникациях — это лишь еще более удобная обертка XMLHttpRequest
.
Безусловно, каждый, кто пишет для веба, должен знать и понимать XMLHttpRequest
, так как это основа веб-коммуникаций, и я не зря по тексту часто даю линки на MDN как первоисточник информации о любой веб-технологии, компоненте или типе данных. Это должно быть ежедневным справочником веб-разработчика.
Также всегда стоит понимать, на каком уровне приложения вы работаете, чтобы не тратить силы и время на операции более низкого уровня. Существует множество как отдельных библиотек, так и фреймфорков, которые избавляют вас от подобной рутины. Но это уже другая обширная тема.
5. Что еще может быть под капотом?
Как я упоминал выше, XML — далеко не самый удобный формат для передачи данных, как и HTTP-запрос — далеко не всегда самый эффективный протокол для обмена данными. Надо постоянно помнить, что есть еще как минимум вебсокеты, реалтайм данные, медиапотоки и т.д. Каждый из них удобен для определенных целей и для определенных типов данных, и на каждом теоретически можно организовать выполнение тех же задач, что и с AJAX.
И если вы работаете на верхнем уровне, то можете даже не догадываться, что творится внутри готового решения. А ведь всегда неплохо понимать, что из доступного инструментария использует компонента фреймворка, чтобы не было «сюрпризов», как с ранними версиями Angular, например, когда приложение неожиданно начинало «жрать» траффик и процессорное время.
И в заключение для тех, кто в ходе чтения еще не залез в консоль своего браузера и не попробовал состряпать тестовый запрос (хотя бы просто для того, чтобы, получив ошибку CORS, понять, что тут рассмотрено далеко не все, о чем надо помнить работая с HTTP-запросами), могу посоветовать пройтись еще раз по теме уже с песочницей на W3School. Это полезный ресурс для новичков.
А для любопытных, вот дополнительный видео-материал с более глубоким анализом AJAX:
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: