Всем известно, что AJAX запросы нужно выполнять с помощью объекта XMLHttpRequest. Но этот объект предоставляет несколько неудобный интерфейс, поэтому с ним мало кто работает напрямую. Вместо этого используются либо самописные легковесные обертки над XMLHttpRequest'ом, либо сторонние библиотеки, которые предоставляют более удобный интерфейс для работы с асинхронными запросами. Давайте притворимся, что мы болеем NIH-синдромом, и попробуем спроектировать собственную библиотеку, удовлетворяющую следующим критериям:
API должен быть простым в использовании
API должен быть гибким
API не должен содержать ничего лишнего
API должен быть ориентирован на создание высокопроизводительных AJAX приложений
размер библиотеки должен быть маленьким
Выбор кодировки запросов и ответов
Вначале определимся с кодировкой данных в запросах и ответах. Название XMLHttpRequest как бы нам намекает, что для кодировки нужно использовать XML. Но мы на это не поведемся, т.к. всем известно, что XML - самый неэффективный способ кодирования данных, передаваемых по сети. Мало того, что он жрет много процессорных ресурсов и памяти при кодировке и раскодировке, так он еще норовит занять весь канал передачи данных своими нескончаемыми мегабайтами тэгов. И даже сжатие ответов на стороне сервера не в силах что-то исправить, т.к. требует дополнительных процессорных ресурсов. Это не выгодно ни владельцам, ни пользователям сервиса.
Популярную до сих пор url encoding тоже не будем использовать по двум причинам:
стандарт url encoding поддерживает кодировку только строковых данных в качестве значений. Т.е. мы должны либо отказаться от аргументво в виде чисел, массивов и вложенных структур данных, либо использовать какую-нибудь самопальную кодировку
браузеры не предоставляют удобных средств для работы с url encoding "искаропки"
Хотя всякие бинарные кодировки типа protocol buffers являются наиболее эффективными с точки зрения использования ресурсов и сжатия передаваемых данных, мы тоже откажемся от них, т.к. опять же браузеры до сих пор не предоставляют удобных средств для работы с ними "искаропки". Надеюсь, это досадное недоразумение будет исправлено в ближайшем будущем.
Остается лишь модный нынче JSON, который намного экономичнее XML'а при передаче сообщений по сети и который хорошо поддерживается современными браузерами. Кроме того, в отличие от url encoding, JSON поддерживает сложные типы данных вроде массивов и вложенных структур. Стоит обратить внимание, что, в отличие от большинства существующих решений, которые используют JSON лишь для кодировки ответов от сервера, а для запросов используется старый-добрый url encoding, у нас JSON будет использоваться на все 100% - как для запросов, так и для ответов. Кодировка запросов с помощью JSON накладывает "существенное" ограничение на метод передачи запросов - метод HTTP GET нам больше не подходит. Вернее, GET все-таки можно использовать, но для этого нужно поверх JSON'а пройтись еще url encoding'ом и молиться, чтобы результирующая строка уместилась в максимальную длину url. Не будем усложнять себе жизнь и вопспользуемся POST'ом.
JsonRpc
С кодировкой определились. Теперь подумаем об удобном интерфейсе. Например о таком:
rpc = new JsonRpc();
rpc.run(rpc_endpoint, request_data, response_callback, finalize_callback);
Где
JsonRpc - обертка над XMLHttpRequest'ом.
rpc_endpoint - url, куда можно отправлять POST запросы, закодированные с помощью JSON. Сервер, находящийся по этому url'у, должен уметь принимать такие запросы и отправлять JSON ответы.
request_data - запрос, представляющий из себя произвольную структуру данных, которая может быть закодирована с помощью JSON, в т.ч. и null.
response_callback - обработчик ответа. Это обычная функция, в которую передается response_data - произвольная структура данных, полученная путем раскодирования ответа. Вызов response_callback может быть пропущен, если произошли ошибки уровня HTTP и JSON encoding'а при выполнении запроса. Для обработки таких ошибок можно воспользоваться finalize_callback'ом.
finalize_callback - "обработчик ошибок". Это функция, которая принимает два параметра - status_code и status_data. status_data - произвольная структура данных, содержащая вспомогательную информацию к status_code. Эта функция гарантированно вызывается для каждого вызова rpc.run() перед тем, как соответствующий запрос будет окончательно завершен. Возможные значения status_code:
SUCCESS - запрос завершился успешно. status_data ничего не содержит
HTTP_ERROR - произошла какая-то ошибка HTTP уровня. status_data содержит соответствующий HTTP status код
JSON_STRINGIFY_ERROR - ошибка при JSON-кодировании запроса. status_data содержит соответствующий эксепшн
JSON_PARSE_ERROR - ошибка при JSON-раскодировании ответа. status_data содержит соответствующий эксепшн
RESPONSE_CALLBACK_ERROR - response_callback выкинул эксепшн, который содержится в status_data
request_data, response_callback и finalize_callback - опциональные параметры, которые могут быть пропущены.
QueuedRpc
Что ж, JsonRpc смотрится неплохо. Но есть небольшая проблемка - rpc.run() не может быть вызван повторно до тех пор, пока не завершился предыдщий запрос. Мы, конечно, можем обойти это ограничение, поместив следующий вызов rpc.run() в finalize_callback предыдущего вызова. Но это не очень красиво. Можно также создать кучу независимых объектов JsonRpc и параллельно дергать их методы run() (или создавать отдельный JsonRpc перед каждым вызовом run()). Но это ресурсоемко и может сильно напрячь наш сервер при большом количестве запросов, если браузер создает отдельные TCP-подключения для каждого объекта XMLHttpRequest. Давайте лушче создадим новый класс QueuedRpc, который будет предоставлять аналогичный метод run(), но с одним отличием - его можно вызывать когда угодно сколько угодно раз. Этот класс выстраивает все запросы в очередь и выполняет их последовательно. Вот его интерфейс:
queued_rpc = new QueuedRpc(base_rpc_call);
queued_rpc.run(rpc_endpoint, request_data, response_callback, finalize_callback);
base_rpc_call - асинхронная функция с параметрами, аналогичными параметрам queued_rpc.run(), которая вызывается последовательно для каждого запроса, помещенного в очередь. QueuedRpc гарантирует, что base_rpc_call не будет вызван повторно до завершения предыдущего запроса. Эта функция должна выполнять всю "грязную" работу по взаимодействию с сервером. Например:
var rpc = new JsonRpc();
var base_rpc_call = function(rpc_endpoint, request_data, response_callback, finalize_callback) {
rpc.run(rpc_endpoint, request_data, response_callback, finalize_callback);
};
Заметьте, что эта функция может перехватывать и подменять все параметры до передачи их в rpc.run(). Например, мы можем с легкостью добавить аутентификацию или обработку server-side ошибок для всех запросов, выполняемых посредством QueuedRpc.run():
var auth_token = 'our-authentication-token';
var auth_rpc_call = function(rpc_endpoint, request_data, response_callback, finalize_callback) {
request_data = [auth_token, request_data];
rpc.run(rpc_endpoint, request_data, response_callback, finalize_callback);
};
var status_check_rpc_call = function(rpc_endpoint, request_data, response_callback, finalize_callback) {
var status_check_response_callback = function(response_data) {
var status_code = response_code[0];
if (status_code != 0) {
alert('unexpected status code='+status_code);
return;
}
response_callback(response_data.slice(1));
};
rpc.run(rpc_endpoint, request_data, status_check_response_callback, finalize_callback);
};
BatchedRpc
ОК, теперь у нас есть клевый класс QueuedRpc, который позволяет минимизировать количество TCP-подключений к серверу при произвольном количестве произвольных AJAX запросов. Но у него опять же есть недостаток - т.к. запросы исполняются последовательно, то мы впустую тратим время на ожидание ответа для каждого запроса. Конечно, можно создать много объектов QueuedRpc на основе различных JsonRpc, и каким-нибудь способом распределять запросы между этими QueuedRpc, тем самым уменьшая среднее время обработки каждого запроса. Но мы снова переходим от одного TCP-подключения к серверу к нескольким. Было бы неплохо и "рыбку съесть и на... сесть" :) Почему бы не отправлять запросы на сервер не по одному, а пачками, тем самым уменьшая накладные расходы и среднее время обработки запроса? Как раз для этого предназначен класс BatchedRpc - он аккумулирует поступившие запросы в течение заданного времени, после чего отправляет их в виде одного запроса на сервер. Вот его интерфейс:
var batched_rpc = new BatchedRpc(base_rpc_call, batch_interval);
batched_rpc.run(request_data, response_callback, finalize_callback);
Рассмотрим новые параметры этого интерфейса.
base_rpc_call принимает на вход такие же параметры, как и batched_rpc.run(). Эта функция вызывается, когда BatchedRpc сформировал пачку запросов для отправки на сервер. request_data, передающийся в base_rpc_call, содержит массив из request_data, переданных в batched_rpc.run() с момента предыдущего вызова base_rpc_call.
batch_interval - время в миллисекундах между последовательными вызовами base_rpc_call. Другими словами, это время, в течение которого могут аккумулироваться поступившие запросы.
Внимательные читатели уже заметили, что в batched_rpc.run() отсутствует параметр rpc_endpoint, который есть в соответствующих методах JsonRpc и в QueuedRpc. Дело в том, что batched запросы не могут иметь произвольный rpc_endpoint. Просто попробуйте представить, как отправить пачку запросов с разными rpc_endpoint'ами (url'ами) в виде одного запроса? Правильный ответ - никак. Поэтому rpc_endpoint для каждого BatchedRpc может задаваться лишь однажды - в соответствующем base_rpc_call. Например:
var rpc_endpoint = '/batched-multiplexor';
var base_rpc_call = function(request_data, response_callback, finalize_callback) {
queued_rpc.run(rpc_endpoint, request_data, response_callback, finalize_callback);
};
Сервер, в свою очередь, должен уметь обрабатывать batched запросы на соответствующем rpc_endpoint'е. Тут никаких сложностей возникнуть не должно, т.к. request_data в этом случае содержит обычный массив, состоящий из request_data batched запросов. Сервер должен сформировать response_data - упорядоченный массив, состоящий из response_data для каждого запроса.
Как же отличать различные методы, если мы используем один rpc_endpoint? Передавайте идентификатор метода в request_data. Например:
В некоторых AJAX приложениях нужно постоянно сохранять на сервере последние изменения, созданные пользователем в браузере. В других приложениях нужно подгружать новые данные с сервера по каждому клику мыши (или даже по каждому перемещению курсора мыши). Как можно уменьшить нагрузку на сервер в этих случаях? Можно пихать все в BatchedRpc. Но это не очень эффективно, если нам важен лишь результат последнего запроса, а все предыдущие запросы можно со спокойной совестью игнорировать. Встречайте - RateLimitedRpc. Этот класс позволяет задавать интервал, в течение которого поступившие вновь запросы игнорируются (кроме последнего). Вот его интерфейс:
rate_limited_rpc = new RateLimitedRpc(base_rpc_call, rate_interval);
rate_limited_rpc.run(request_data, response_callback, finalize_callback);
В base_rpc_call передаются те же параметры, что и в rate_limited_rpc.run(). base_rpc_call вызывается лишь тогда, когда RateLimitedRpc решает передать запрос на сервер, т.е. где-то один раз в rate_interval миллисекунд.
finalize_callback, переданный в rate_limited_rpc.run(), вызывается для каждого запроса, даже если он был проигнорирован. Это позволяет отслеживать проигнорированные запросы. Например:
var request_data = 123;
var is_called = false;
var finalize_callback = function(status_code, status_data) {
if (status_code == JsonRpc.statusCodes.SUCCESS && !is_called) {
alert('the request has been ignored. request_data='+request_data);
}
};
var response_callback = function(response_data) {
is_called = true;
// process response_data here.
};
rate_limited_rpc.run(request_data, response_callback, finalize_callback);
Заключение
Невероятно, но факт - в то время, как вышеописанный функционал критичен при написании высокопроизводительных AJAX приложений, он почему-то отсутствует в популярных AJAX библиотеках. В итоге получаем тормозные веб2.0 приложения, жрущие ваш мобильный трафик. Черт с ним с трафиком пользователя, но ведь владелец приложения выкидывает на ветер намного больше денег за бесполезный траф и лишнюю нагрузку на сервера.
Код этой библиотеки пока еще отсутствует в свободном доступе. Я выложу ссылку в комментарии к этой статье после того, как опубликую код. Его лицензия - BSD, так что можете свободно использовать его в своих коммерческих проектах. Может, кому-нибудь он окажется полезным.
Edit:
Вот обещанные исходники - https://github.com/valyala/hpajaxrpc .
Т.к. на dev.by сейчас поломано добавление комментов (http://dev.by/forum/post/11737/ ), то отвечу отписавшимся тут:
Я магу толькі параіць напісаць яшчэ PeriodicalRpc для аднаўлёньня якога-небудзь элементу праз некаторы час.
PeriodicalRpc весьма сомнителен в плане дизайна и юзабельности. Намного проще написать
Python или JavaScript: что выбрать в 2025 году? Смотрим различия и рекомендуем годные курсы
Выбор языка программирования — это стратегия вашего развития в IT. Какой язык откроет для вас больше возможностей в 2025 году: Python или JavaScript? Пробуем разобраться и не включать холивар.
Фронтендер — это фуллстак. Примите реальность. Разработчик рассказывает, как изменился рынок
Работы для джунов очень мало: у компаний ограниченные бюджеты, а ИИ становится всё более популярным. Технологии постоянно меняются, и объём теоретической «базы» значительно вырос. Фреймворки каждый год добавляют новые возможности.
Расскажу, как обстоят дела на рынке фронтендеров.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.