Close

16.10.2016

Асинхронщина на web страницах

Представьте: Вам надо познакомить новичка в вебе с AJAX технологиями.

Надо дать  обзор, показать? Как этим пользуются сейчас, типа  «CookBook»,  и показать

основные паттерны.

В этой статье я собираюсь выложить последовательный экскурс в AJAX like техники.

Туда — Сюда

«Откуда проявляется инициатива соединения? C браузера или сервера&» — не злободневный сегодня вопрос. Раньше она всегда исходила с браузера, но сегодня развивается и обратаная сторона медали.

Для проявления инициативы с браузера используют AJAX.

Для проявления инициативы с сервера — некие COMET  PUSH-технологии, которые лучше  рассматривать в зависимости от ситуации.

Здесь мы кратко, но понятно рассмотрим и первые, и вторые.

Что такое AJAX?

Очень много букв тут (https://ru.wikipedia.org/wiki/AJAX).  Просто надо прочитать, повторять тут не буду стандартные слова.

Как пользоваться этим «нативно»

На низком уровне реализован AJAX с помощью XMLHTTPRequest.

Если он реализован иначе, то «обертки драйвера» предоставят схожий API.

XMLHTTPRequest — основа AJAX. Отличается все только тем, как его использовать, как получать.

Непосредственно его самого можно  использовать, что называется «руками» или «нативно».

Про это  написано хорошо тут http://javascript.ru/ajax/transport/xmlhttprequest , коротко суть в следующем:

имеются методы

open(Method, Url, async)
send(data)
onreadystatechange

вызываются в таком порядке

req.open("GET", url, true)
req.onreadystatechange = handler
req.send(null)

можно поработать с заголовками запроса и ответа

setRequestHeader()
getResponseHeader()
getAllResponseHeaders()

так можно обрывать соединение

abort()

в обработчик приходит ответ сервера с полезными полями, Вот полезные поля:

responseText
responseXML
status
statusText

Причем responseXML заполняется только в случае, когда Content-Type с сервера имеет значение text/xml (кроме overrideMimeType-метода, но он есть только в Firefox).

Пример использования XMLHTTPRequest:

function getXmlHttp(){
	try {
		return new ActiveXObject("Msxml2.XMLHTTP");
	} catch (e) {
		try {
			return new ActiveXObject("Microsoft.XMLHTTP");
		} catch (ee) {
		}
	}
	if (typeof XMLHttpRequest!='undefined') {
		return new XMLHttpRequest();
	}
}

// Получить данные с url и вызывать cb - коллбэк c ответом сервера 
function getUrl(http://javascript.ru/forum/url, cb) { 
	var xmlhttp = getXmlHttp();
	// IE кэширует XMLHttpRequest запросы, так что добавляем случайный параметр к URL
	// (хотя можно обойтись правильными заголовками на сервере)
	xmlhttp.open("GET", url+'?r='+Math.random());
	xmlhttp.onreadystatechange = function() {
		if (xmlhttp.readyState == 4) {
			cb(
			xmlhttp.status, 
			xmlhttp.getAllResponseHeaders(), 
			xmlhttp.responseText
			);
		}
	}
	xmlhttp.send(null);
}

углубляться дальше не будем — ведь есть про эту штуку целый сайт http://xmlhttprequest.ru/

Надо сказать,  что в реальной жизни пользуются этим очень редко. Пользуются удобными обертками, о них и пойдет дальше речь.

Почему редко пользуются ? Просто реализовано в разных браузерах по-разному. Вот НЕПОЛНЫЙ пример «кроссбраузерного использования» (есть и лучше, но не будем тратить место):

function getXmlHttp(){
  var xmlhttp;
  try {
    xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) {
    try {
      xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (E) {
      xmlhttp = false;
    }
  }
  if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
    xmlhttp = new XMLHttpRequest();
  }
  return xmlhttp;
}

Prototype

prototype js api понимает такую постановку запроса

new Ajax.Request('/your/url', {
  onSuccess: function(response) {
    // Handle the response content...
  }
});

обработка запроса проходит следующие фазы:

  1. Создание
  2. Инициализация
  3. Отправка запроса
  4. Прием ответа (может происходить несколько раз, если данные приходят порциями)
  5. Ответ принят, Запрос завершен

Каждую фазу можно обработать обработчиками. Prototype понимает различные виды «mime javascript» ответов и преобразует параметры вызова к объектам, возможна гибкая работа с заголовками.

Но в свое время prototype потерял популярность и уступил jQuery. Почему ? Ну, он немного расширял стандартные классы JS своими методами, а когда стандарт JS начал меняться, это стало проблемой. Мораль: не расширяй API стандарт. языка — делай свою либу сбоку (в назидание Ruby RoR).

YUI

Нельзя пройти мимо, ведь YUI исповедует необычный подход. У них есть модуль «io», и работа с ajax происходит как бы в процедурном стиле.

Это лучше всего показать на примере:

// Create a YUI instance using io module.
YUI().use("io-base", function(Y) {
    /*
     * Create a function as the event handler for the event "io:complete".
     *
     * The function will receive the following arguments:
     * - The ID of the transaction
     * - Object containing the response data.
     * - Argument one defined when subscribing to the event(e.g., "foo").
     * - Argument two defined when subscribing to the event(e.g., "bar").
     */
    function onComplete(transactionId, responseObject, arg1, arg2) {
        /*
         * The argument 'responseObject' is the response object.  Its
         * properties are:
         * - status
         * - statusText
         * - getResponseHeader(headerName)
         * - getAllResponseHeaders
         * - responseText
         * - responseXML
         *
         * NOTE: In an XDR transaction, only the responseText or the responseXML property is defined.
         */
    };

    /*
     * Subscribe to the event "io:complete", using Y.on.
     *
     * - 'io:complete' : Subscribe to this io event.
     * - onComplete : The event handler to be subscribed to 'io:complete'.
     * - Y : The execution context of the event handler, in this case, the YUI sandbox.
     *       since the doComplete is defined as a global function.
     * - 'foo' : The first argument received by the event handler.
     * - 'bar' : The second argument received by the event handler.
     *           Additional arguments can be defined, as desired.
     */
    Y.on('io:complete', onComplete, Y, "foo", "bar");

    // Starts the transaction.
    var request = Y.io(uri);
});

Видите ? Настраиваются реакции на start, complete, success, failure, end события и делается просто Y.io(uri);

Ну… Так тоже можно. По мне, так «вывих мозга».

Ext JS

Пример из 4.0 версии EXT JS

Ext.Ajax.request({
    url: 'page.php',
    params: {
        id: 1
    },
    success: function(response){
        var text = response.responseText;
        // process server response here
    }
});

Это уже похоже на Jquery код — типичный представитель callback техники.
Параметров хватает для работы. В остальном ни хуже, ни лучше — просто нормально.
Ссылка на описание http://www.objis.com/formationextjs/lib/extjs-4.0.0/docs/api/Ext.Ajax.html

Dojo

DOJO — один из лучших в мире движков или библиотек. Что это конкретно — тут трудно разделить.

Он на голову выше стоящих рядом побратимов. Уже много лет подряд самые передовые фичи появляются сначала в DOJO, а уж потом в прочих библиотеках.

От остальных он отличается системностью подхода. И структура, и модульность, и степень декларативности, и прочие черты выделяют его. Он профессионален, несомненно.

Сборка, деплой, расширение, виджеты, работа с DOM, события, анимации — все это есть там с незапамятных времен из коробки, и это удобно, но к этому обилию нужно привыкать. Кроме того, такое «погружение»  выдирает тебя из остального мира, ты будешь видеть только кишки dojo, его будет хватать. И не все готовы платить такую цену. Так что,  полнота — это наверное и достоинсво, и недостаток.

Простой пример использования dojo для ajax:

(показана еще и разметка, например, для отображения состояния процесса запроса)

<style type=”text/css” > 
 .success {
 background-color: #dfd;
 border: 2px solid #000;
 }
 .error {
 background-color: #fdd;
 border: 2px dotted #000;
 }
 < /style > 
 < div id=”xhrget1” > 
 < h2 > Simple xhrGet < /h2 > 
 < p class=”content_found” > Loading... < /p > 
 < p class=”content_not_found” > Loading... < /p > 
 < /div >

и вот такой, например, код:

dojo.xhrGet({
 url: ‘data.txt’,
 timeout: 5000,
 load: function(resp, io_args) {
    dojo.query(‘.content_found’, ‘xhrget1’)
       .addClass(‘success’)
       [0].innerHTML = resp;
 },
 error: function(error, io_args) {
    dojo.query(‘.content_found’, ‘xhrget1’)
      .addClass(‘error’)
      [0].innerHTML = error.message;
 }
});

Поддерживает кучу всего, например, Deferreds, таймауты и т.д.
Еще пример:

require(["dojo/request"], function(request){
    request.post("post-content.php", {
        data: {
            color: "blue",
            answer: 42
        },
        headers: {
            "X-Something": "A value"
        }
    }).then(function(text){
        console.log("The server returned: ", text);
    });
});

Здесь используется модульность dojo и deffered в короткой цепочке.
Код у dojo простой и лаконичный (кому как).
Вот пример, где модульность используется более оправдано:

require(["dojo/_base/xhr", "dojo/dom", "dojo/_base/array", "dojo/domReady!"],
function(xhr, dom, arrayUtil) {
  // Keep hold of the container node
  var containerNode = dom.byId("newsContainerNode");
  // Using xhr.get, as we simply want to retrieve information
  xhr.get({
    // The URL of the request
    url: "get-news.php",
    // Handle the result as JSON data
    handleAs: "json", 			
    // The success handler
    load: function(jsonData) {
      // Create a local var to append content to
      var content = "";
      // For every news item we received...
      arrayUtil.forEach(jsonData.newsItems, 
	function(newsItem) {
	  // Build data from the JSON
	  content += "
» + newsItem.title + «

«; content += » » + newsItem.summary + » «; }); // Set the content of the news node containerNode.innerHTML = content; }, // The error handler error: function() { containerNode.innerHTML = «News is not available at this time.»; } }); } );

Отмечу только, что все эти ООП фишки DOJO трудно использовать, потому что JS. (субъективно)

MooTools

Смотри сюда http://mootools.net/core/docs/1.5.2/Request/Request
Пример:

var myRequest = new Request({
    url: 'image.jpg',
    onProgress: function(event, xhr){
        var loaded = event.loaded, total = event.total;

        console.log(parseInt(loaded / total * 100, 10));
    }
});

myRequest.send();

Тоже хорошая юзабельная обертка, но JQ все же более популярен.

К Mootools стоит присмотреться из-за предлагаемого им способа ООП, ставшего популярным, Core, Сlass — вот , что в этой либе сделано не плохо.

JQUERY

Самый популярный сегодня в мире способ делать AJAX запросы.

Он вобрал в себя «все», а если этого нет, то можно подключить плагины, которых тысячи. Как он стал так популярен — вопрос, достойный отдельного ислледования.

Для AJAX основные методыфункций это:

  • jQuery.ajax( url [, settings ] )
  • jQuery.get( [settings ] )
  • jQuery.post( url [, data ] [, success ] [, dataType ] )

Есть много других функций. Например, для работы с контентом

  • .load( url [, data ] [, complete ] )
  • пример использования: $( "#result" ).load( "ajax/test.html" );

    здесь в элемент DOM с id=»result» спустя время запишется html содержимое которое вернет запрос заданный параметрами в load

Плный список методов функций категории AJAX http://api.jquery.com/category/ajax/

Про праметры можно прочитать в документации. Студентам же важно понять отличие GET и POST.
GET и POST — методы HTTP протокола. Лучше всего: просто прочитать спецификацию HTTP протокола и вопрос сам собой отпадет. Лучше прочитать еще и идеологию REST, которая расставляет акценты в HTTP и его использовании.

Коротко: get для получения контента, POST — для обновления, отправки.  Вероятно, что GET запросы будут кешироваться по пути к конечному серверу, т.к. проксями рассатриваются как просто получение данных.  Поэтому используйте POST, если запрос на сервере что-то делает. Стоит прочитать еще про спец. поля заголовка, про ETag, чтобы эффективней бороться с желанием посредников отдать вам ответ из кеша. Ссылка  http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9

comet PUSH в браузер

Ооой, тяжкая тема. Трудно в двух словах. Это связано с хорошим и не хорошим опытом, но попробую.

Технически, бОльшая часть, темы раскрыта тут http://javascript.ru/ajax/comet. Прочитайте, полностью повторяться не охота. Дополню не раскрытые, на мой взгляд, пункты.

WS — Web Socket

Есть, например,  WS = WebSocket. Каждый раз, когда я решал, что он готов к использованию, я попадал на грабли и видел, что зря я решил его использовать. Проблемы были, например, такие:

  1. Есть в нем фаза рукопожатий, а самого WS 100500 версий (drafts) и не понятно с какой версией протокола к вам прийдет клиент — значит, сервер должен их все уметь, а мне надо было сервер тогда делать на C++…. Это был ахтунг вообще! (клиет тоже был на плюсах)
  2. Версии, вроде, устаканились, но их все равно много. В итоге, не каждый сервер и не каждый клиент могут соединиться. Если клиент — браузер, то хоть половину проблем это «решает» (они не ваши), а выбор серверов сужается.
  3. Время жизни соединений. С виду WS соединение -это HTTP, но только с виду, и долгоживущее. Для проксей роутеров и маршрутизаторов итд это непривычно. Допустим, мы настроили наше ПО, настроили балансировщики, чтобы наши «постоянные соединения» они не рвали, НО ! все равно у закзачика ты не знаешь, какая будет сеть и как ходят пакеты. И не понятно, почему у него в рамках содинения ну никак не идет больше 100 Метров за коннект и почему соединения не живут больше трех дней. Никак это иногда не выяснить (заказчик и сам может не знать), и ping-pong не помогает. Приходится пересоединяться. А если так,  то чем LongPooling хуже? Даже еще лучше — он хоть вообще родной для http.
  4. Например, java world плохо работает с асинхронщиной в вебе и с WS в частности. Для сервлетов это проблема, которую не сразу понятно, как решать. И многие one shot каркасы не умеют асинхроные WS транспорты (сейчас ситуация сильно поменялась).

И больше наступать на эти грабли нет никакого желания. 🙁

Long Poling

Суть такая http://javascript.ru/ajax/comet/long-poll.

Если транспорт нужно делать руками, то это  лучший вариант, я считаю. Никогда не подводил.

Тупой «просто poling» рассматривать вообще не будем — не вариант.

text/event-stream

Такой mime  (text/event-stream) есть в спецификации. А в JS есть EventSource.

Этим примерно так можно пользоваться https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events

Я  попробовал. Оно хорошо работает, однако, не во всех браузерах. Не работает никогда в IE и в OperaMini. В Остальных браузерах — гуд.

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

Поддержка с серверной стороны плохая из коробки, но особо и не нужна — вы можете сами это сделать элементарно, например, в tornado.

Socket.io

Это не метод IO, а либа (и сервис).

Идея в том, что у кого что доступно — и это низкий уровень этой либы, а на высоком уровне она просто дает тебе транспорт, решает проблему выбора и сводит выбор к одной только себе.

Ссылка http://socket.io/, лицензия MIT.  github.

Интересна тем, что предоставляет связь не только клиент-сервер, но и клиент-клиет из коробки. Это позволяет не влезать в ее серверную часть, а просто развернуть ее у себя, как есть (nodejs). Меньше отладки на севере (ноль).

Прекрасная поддержка есть у Python Tornado (думаю, лучший событийный однопоточный движок), но для java только нашел кое что, но не пробовал, а интересно попробовать.

Поддержка tornado

Поддержка в java

… для java ни LP ни WebSocket не работают, я так думал, но оказалось, что
https://toster.ru/q/59730 их «полно» для socket.io, правда, не знаю, какого качества, но они есть, и, значит, тем более стоит попробовать.

Перечислены следующие варианты:

В socket.io есть «комнаты» для общения клиентов. Это похоже на jabber\xmpp, только работает не бинарно, а в браузере. Подобную идеологию также продвигают иные движки, например, с тегом IOT (Internet Of Things), например, JetBus.

JetBus

ссылка: http://jetbus.io/

Не самый популярный, просто написал о нем, т.к. мы использовали его. Сервер у них есть на node.js и на моем любимом LUA — плюсик, кроме того, API имеет более высокий уровень. Короче, мы попробовали переписать один из проектов на нем (Presentator).

Не буду вдаваться в идеологию этой системы — у них на сайте все хорошо разжевано, но кое во что мы уперлись.

Структура значений там состоит из ключей и подключей итд. Так вот, мы ожидали, что порядок событий об обновлении будет совпадать с тем порядком, в котором мы значения меняли, НО порядок уведомлений не совпадает с порядком внесения изменений в состояние.

Эта проблема может вам помешать, помешала она и нам. В итоге, мы написали (нам пришлось) свой сервер jetbus на питоне, который схраняет порядок установки ключей.

Больше я его использовать не стану, тем более, что по концепции MQTT брокеры похожи на jetbus, выше по производительности и протокол стандартный, плюс много обёрток и библиотек для разных языков.

MQTT брокеры

Посмотрите этот бенч. http://www.scalagent.com/IMG/pdf/Benchmark_MQTT_servers-v1-1.pdf

От себя добавлю, что мы в KV сервисе всё же используем WS а не MQTT. Почему ?

При высоких нагрузках становится ценным каждое соединение и то, что в MQTT и WS в одном соединении может ходить масса запросов и ответов (фреймов) это хорошо.

Но MQTT PUB-SUB паттерн плохо ложится на message passing, в рамках которого легче описывать распределённые алгоритмы. А поскольку MQTT «data format agnostic» и WS тоже, то всё равно задача кодирования должна решаться пользователем и никакого преимущества тогда MQTT над WS не имеет.

Кроме того, WS (который поверх HTTP) проще балансировать к бекендам, чем это пришлось бы делать на MQTT через бинарный balancer, например, haproxy.

 

 

 

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *