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. Немного практики

Тут надо сделать ремарку, что «практических точек зрения» всегда несколько и они зависят от уровня решаемой задачи. Если брать самый верхний уровень — презентации страницы, — то разработчику надо выполнить как минимум следующие действия:

  1. определить часть страницы, которая зависит от внешних данных;
  2. описать, с какими данными и как эта часть страницы будет связана… и все.

В целом, этого достаточно для понимания, что именно и когда надо перерисовывать на странице.

«И где тут AJAX?», — спросите вы.

А нигде! Для этого уровня разработки вообще не важно, как вы связываете данные и DOM. Чтобы столкнуться с AJAX, надо спуститься минимум на уровень ниже, где вы определяете компоненты страницы.

Тут задач уже побольше:

  1. определить момент, когда данные устарели;
  2. выдать запрос серверу (AJAX);
  3. получить данные от сервера (AJAX);
  4. проверить/обработать полученные данные (+/- AJAX);
  5. обработать состояния ошибок (+/- AJAX);
  6. выполнить действия со страницей в соответствии с данными.

Причем задачи 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:

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023