- PVSM.RU - https://www.pvsm.ru -
Здравствуйте, в этой маленькой заметке расскажу немного про ООП в JS, объект XMLHttpRequest, паттерн прокси, и дружелюбие джаваскрипта в этом плане.
Была у меня сегодня такая задача — есть проект, который довольно активно использует ajax-запросы, но вот проблема — бекенд у нас так устроен, что разаутентифицирует пользователя, если тот не активен в течение, скажем, полу часа. В итоге случалось такое, что пользователь, пытаясь совершить какое-то действие, которое использует аякс, не мог его совершить (уж извините за тавтологию), нужно было решить эту проблему.
Если совершается аякс-запрос и в ответ приходит *что-то, говорящее о том, что у пользователя завершилась сессия*, нужно отобразить пользователю форму входа (обычным оверлейем, без всяких айфреймов), и дать ему возможность аутентифицироваться через нее (опять же по аяксу, т.к. нельзя потерять состояние страницы). Более того, если он пройдет аутентификацию, аякс-запросы, которые не прошли тогда, нужно отправить заново. Но вот первая проблема — нужно это сделать так, чтобы js-код, который опирается на этот запрос ничего не почувствовал, то есть нужно чтобы все callback-и сработали как надо и когда надо (они не должны сработать тогда, когда окажется, что нужна аутентификация, но должны тогда, когда она будет пройдена). И вторая проблема идет от асинхронности запросов — их может быть много, может получиться так, что сразу несколько запросов столкнуться с этой проблемой, надо наблюдать за всеми и, если нужно, перезапускать их после аутентификации. И, да, *что-то, говорящее о том, что у пользователя завершилась сессия* — в нашем случае это код ответа «403» и тело ответа «401» (401, потому что близко по духу, но нельзя из-за потребности в WWW-Authenticate, а просто 403 нельзя т.к. вообще по-хорошему с аутентификацией это никак не связано, но, хотя бы, близко).
Не долго думая я пришел к решению — воспользовавшись паттерном «прокси», создать, собственно, объект-проксю, и подменить им XMLHttpRequest (XHR), а в самом этом проксе уже общаться напрямую с XHR-ом. И да, представьте себе, в Javascript-е возможно подменить класс другим классом, фактически тут класс — это тот же объект (или прототип, в терминологии я не силен :( ).
Итак для начала, как же вообще создавать класс, инстанс которого можно будет получить с помощью new ClassName() и как вообще туда добавлять методы и свойства? Тут есть несколько способов, чтобы увидить все можете погуглить [1], я воспользовался наверное самым простым, вот как выглядит определение класса у нас:
(function () {
"use strict";
window.SomeClass = function () {
var randNumber = Math.random();
this.someMethod = function () {
console.log(randNumber);
};
this.randomized = randNumber;
};
})();
Если вы заметили, я сразу упаковал весь код в функцию, которую тут же вызвал, это обычная практика в JS, она используется во-первых для чистоты кода (не засоряем глобальный скоуп), и во-вторых из-за производительности (это вытекает из первого, дело в том, что когда вы создаете очень много переменных в одной области видимости (в данном случае в глобальной), то при обращении к ним, интерпретатор будет искать их дольше, т.к. ему придется перебрать больше вариантов, ведь поиск переменной начинается с самого близкого скоупа и идет вверх до глобального). Так же я использую use strict, можете почитать о нем здесь [2], он поможет избежать вам непредвиденных ситуаций, особенно если вы используете IDE (и особенно если используете JsLint/JsHint). И о коде – как видите мы создали «класс» SomeClass в глобальной области видимости, фактически конструктором этого класса является весь код внутри функции. В итоге мы имеем переменную randNumber, которая видная только изнутри класса (точнее его экземпляра), метод someMethod(), который шлет в консоль всегда одно и то же число для одного и того же экземпляра класса, и свойство randomized, которое равно этому же числу.
Делаем подмену:
(function () {
"use strict";
// сохраним оригинальный объект, т.к. без него не сможем слат запросы
var XHR = window.XMLHttpRequest;
window.XMLHttpRequest = function () {
// создаем экземпляр оригинала
var o = new XHR(),
t = this,
reassignAllProperties = function reassign() {
t.readyState = o.readyState;
t.responseText = o.responseText;
t.responseXML = o.responseXML;
t.status = o.status;
t.statusText = o.statusText;
};
t.readyState = 0;
t.responseText = "";
t.responseXML = null;
t.status = null;
t.statusText = "";
// просто подменим все методы, не меняя никак поведение
// но добавим вызов reassignAllProperties() т.к. после
// вызова любого из методов может быть изменено какое-то св-во
t.open = function open() {
o.open.apply(o, arguments);
reassignAllProperties();
};
t.send = function send() {
o.send.apply(o, arguments);
reassignAllProperties();
};
t.abort = function abort() {
o.abort();
reassignAllProperties();
};
t.setRequestHeader = o.setRequestHeader;
t.overrideMimeType = o.overrideMimeType;
t.getResponseHeader = o.getResponseHeader;
t.getAllResponseHeaders = o.getAllResponseHeaders;
t.onreadystatechange = function () {};
o.onreadystatechange = function onReady() {
reassignAllProperties();
t.onreadystatechange();
};
};
})();
Как видно из кода, он полностью повторяет оригинальный XMLHttpRequest (сводку по методам/св-вам можете посмотреть на русской страничке википедии [3]). Нам же нужно намного больше – надо следить за ответом сервера, и если заметим 403-й ответ и 401 в теле, то в срочном порядке открываем форму логина. Но пользовательский callback при этом не должен быть вызван. И более того, после «аборта» должна быть возможность перезапустить запрос и получить ответ. Следовательно мы должны хранить в объекте-прокси все данные, которые передавались каким-либо методам-сеттерам (в т.ч. open и send) и при перезапуске запроса нужно заново вызвать все эти методы. Но вот проблема остается – представим ситуацию, когда был совершен запрос, окончился неудачей из-за разаутентификации, тогда, только после того как юзер залогиниться, мы должны перезапустить запрос и запустить событие onreadystatechange, но это событие не должно быть запущено до того как запрос будет запущен повторно. Решение простое – дело в том, что событие onreadystatechange запускается по крайней мере четыре раза, при этом свойство readyState инкрементируется (тут все его значения [4]), так что нам нужно вызвать пользовательский callback только тогда, когда мы будем уверены, что ответ легитимный. Но, если где-то используются состояния отличные от «complete», нужно это учесть, проще всего тогда будет запустить событие три раза с тремя последними readyState (от 2 до 4), просто в цикле. Также нужно хранить все запросы, завершившиеся неудачей, которые нужно будет перезапустить после этого.
(function () {
"use strict";
// сохраним оригинальный объект, т.к. без него не сможем слать запросы
var XHR = window.XMLHttpRequest,
// здесь будет хранить все завершившиеся неудачей запросы, которые ожидают ретрая
failedRequestsPool = [],
authenticationWindow = function () {
$("#auth-overlay").show();
};
$("#auth-overlay form").submit(function () {
$.ajax({
type: "post",
url: "/login",
data: {
login: $("#auth-login").val(),
password: $("#auth-password").val()
},
dataType: "json",
success: function (data) {
if (data.state === "OK") {
$("#auth-overlay").hide();
// если прошли логин, нужно перезапустить все ожидающие запросы
for (var i in failedRequestsPool) {
if (failedRequestsPool.hasOwnProperty(i)) {
failedRequestsPool[i].retry();
}
}
failedRequestsPool = [];
}
}
});
return false;
});
window.XMLHttpRequest = function () {
// создаем экземпляр оригинала
var o = new XHR(),
t = this,
// этот флаг понадобится, чтобы не пропустить плохой ответ до callback-а
aborted = false,
reassignAllProperties = function reassign() {
t.readyState = o.readyState;
t.responseText = o.responseText;
t.responseXML = o.responseXML;
t.status = o.status;
t.statusText = o.statusText;
},
// будем хранить все переданные данные здесь, чтобы при ретрае снова передать их
data = {
open: null,
send: null,
setRequestHeader: [],
overrideMimeType: null
};
t.readyState = 0;
t.responseText = "";
t.responseXML = null;
t.status = null;
t.statusText = "";
t.retry = function retry() {
aborted = false;
// снова передаем все данные
o.open.apply(o, data.open);
reassignAllProperties();
for (var i in data.setRequestHeader) {
if (data.setRequestHeader.hasOwnProperty(i)) {
o.setRequestHeader.apply(o, data.setRequestHeader[i]);
}
}
if ("overrideMimeType" in o && data.overrideMimeType !== null) {
o.overrideMimeType(data.overrideMimeType);
}
o.send(data.send);
reassignAllProperties();
};
// просто подменим все методы, не меняя никак поведение
// но добавим вызов reassignAllProperties() т.к. после
// вызова любого из методов может быть изменено какое-то св-во
t.open = function open() {
data.open = arguments; // запомним, для случая ретрая
o.open.apply(o, arguments);
reassignAllProperties();
};
t.send = function send(body) {
data.send = body;
o.send(body);
reassignAllProperties();
};
t.abort = function abort() {
o.abort();
reassignAllProperties();
};
t.setRequestHeader = function setRequestHeader() {
data.setRequestHeader.push(arguments);
o.setRequestHeader.apply(o, arguments);
};
// зметьте что в IE может не быть этого метода, поэтому проверим
if ("overrideMimeType" in o) {
t.overrideMimeType = function (mime) {
data.overrideMimeType = mime;
o.overrideMimeType(mime);
};
}
t.getResponseHeader = o.getResponseHeader;
t.getAllResponseHeaders = o.getAllResponseHeaders;
t.onreadystatechange = function () {};
o.onreadystatechange = function onReady() {
reassignAllProperties();
// если еще не остановили запрос и если видим при этом, что нужно, то останавливаем
if (!aborted && o.state === 403 && o.responseText.indexOf("401") !== -1) {
aborted = true;
o.abort();
failedRequestsPool.push(t);
authenticationWindow();
}
// если не был остановлен и уже готов ответ, то даем знать
if (!aborted && o.readyState === 4) {
for (var i = 1; i < 5; ++i) {
t.readyState = i;
t.onreadystatechange();
}
}
};
};
})();
Вот так, довольно просто, мы подменили XHR на прокси, который не даст упустить запросы, отправленные после «разаутентификации» пользователя.
ПС я заметил одну ошибку, метод getAllResponseHeaders() в опере выкидывает WRONG_THIS_ERR, только совершенно непонятно откуда это.
Автор: nikita2206
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/11738
Ссылки в тексте:
[1] погуглить: https://www.google.com/search?q=javascript+class+definition
[2] здесь: https://developer.mozilla.org/en/JavaScript/Strict_mode
[3] страничке википедии: http://ru.wikipedia.org/wiki/XMLHttpRequest
[4] значения: http://msdn.microsoft.com/en-us/library/ie/ms534361(v=vs.85).aspx
Нажмите здесь для печати.