Support us

Проектирование эффективной библиотеки для выполнения AJAX запросов

Оставить комментарий
Проектирование эффективной библиотеки для выполнения AJAX запросов

Введение

Всем известно, что 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. Например:
batched_rpc.run(['sum', [1,2]], response_callback);
batched_rpc.run(['mul', [3,4]], response_callback);

RateLimitedRpc

В некоторых 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 весьма сомнителен в плане дизайна и юзабельности. Намного проще написать
setInterval(function() { rpc.run(request_data); }, interval);
вместо
base_rpc_call = function(request_data, response_callback, finalize_callback) {
  rpc.run(request_data, response_callback, finalize_callback);
};
rpc = new PeriodicalRpc(base_rpc_call, interval);
rpc.run(request_data);
Насчет комментария AmdY мне сказать нечего :)
Место солидарности беларусского ИТ-комьюнити

Далучайся!

Читайте также
10 популярных курсов по изучению JavaScript для крутой веб-разработки
10 популярных курсов по изучению JavaScript для крутой веб-разработки
10 популярных курсов по изучению JavaScript для крутой веб-разработки
JavaScript остается одним из самых популярных языков программирования в мире. Мы собрали список курсов и сертификаций по Javascript от основ до необычных особенностей. В листинге как платные, так и бесплатные онлайн-курсы. Погнали за новыми знаниями!
2 комментария
Как оплачиваются самые популярные языки GitHub и какой прогноз
Как оплачиваются самые популярные языки GitHub и какой прогноз
Как оплачиваются самые популярные языки GitHub и какой прогноз
Rust стал самым быстрорастущим языком по числу разработчиков
Rust стал самым быстрорастущим языком по числу разработчиков
Rust стал самым быстрорастущим языком по числу разработчиков
Бесплатные курсы по TypeScript, React, 3D разработке. По итогам могут взять на работу
Бесплатные курсы по TypeScript, React, 3D разработке. По итогам могут взять на работу
Бесплатные курсы по TypeScript, React, 3D разработке. По итогам могут взять на работу

Хотите сообщить важную новость? Пишите в Telegram-бот

Главные события и полезные ссылки в нашем Telegram-канале

Обсуждение
Комментируйте без ограничений

Релоцировались? Теперь вы можете комментировать без верификации аккаунта.

Комментариев пока нет.