Введение
Всем известно, что AJAX запросы нужно выполнять с помощью объекта XMLHttpRequest. Но этот объект предоставляет несколько неудобный интерфейс, поэтому с ним мало кто работает напрямую. Вместо этого используются либо самописные легковесные обертки над XMLHttpRequest'ом, либо сторонние библиотеки, которые предоставляют более удобный интерфейс для работы с асинхронными запросами. Давайте притворимся, что мы болеем NIH-синдромом, и попробуем спроектировать собственную библиотеку, удовлетворяющую следующим критериям:- API должен быть простым в использовании
- API должен быть гибким
- API не должен содержать ничего лишнего
- API должен быть ориентирован на создание высокопроизводительных AJAX приложений
- размер библиотеки должен быть маленьким
Выбор кодировки запросов и ответов
Вначале определимся с кодировкой данных в запросах и ответах. Название XMLHttpRequest как бы нам намекает, что для кодировки нужно использовать XML. Но мы на это не поведемся, т.к. всем известно, что XML - самый неэффективный способ кодирования данных, передаваемых по сети. Мало того, что он жрет много процессорных ресурсов и памяти при кодировке и раскодировке, так он еще норовит занять весь канал передачи данных своими нескончаемыми мегабайтами тэгов. И даже сжатие ответов на стороне сервера не в силах что-то исправить, т.к. требует дополнительных процессорных ресурсов. Это не выгодно ни владельцам, ни пользователям сервиса. Популярную до сих пор url encoding тоже не будем использовать по двум причинам:- стандарт url encoding поддерживает кодировку только строковых данных в качестве значений. Т.е. мы должны либо отказаться от аргументво в виде чисел, массивов и вложенных структур данных, либо использовать какую-нибудь самопальную кодировку
- браузеры не предоставляют удобных средств для работы с url encoding "искаропки"
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
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. Другими словами, это время, в течение которого могут аккумулироваться поступившие запросы.
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 мне сказать нечего :)
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.