- PVSM.RU - https://www.pvsm.ru -
Привет!
Сегодня мы хотели бы порассуждать о promises [1] и повторном использовании кода.
Так получилось, что две эти вещи вкупе несколько раз очень сильно нас выручили, и мы хотели бы поделиться тем, как это было.

Мораль проста: если вы еще не используете promise-ы, начните это делать!
Мы уже вкратце рассуждали на эту тему в двух наших статьях (один [2], ноль [3]). Собственно, эта статья должна была быть финальным разделом к предыдущей [2]. Но там чтиво тяжелое и большое, поэтому решили разбить на две.
Здесь мы расскажем несколько реальных историй о том, как они нас спасали.
Небольшое предисловие: в одном из наших приложений все запросы к back end отправляются через центральную точку — метод callService базового модуля, от которого наследуют все сервисы. На вход этот метод принимает параметры запроса, нормализует их и возвращает promise, на который вызывающая сторона может подписаться. Примерно вот так:
callService: function (settings)
{
//Нормализуем параметры вызова
var callSettings = {
type: settings.method || ETR.HttpVerb.POST,
contentType: 'application/json; charset=utf-8',
dataType: settings.dataType || undefined,
url: settings.relativeUrl ? ETR.serviceRootURL + settings.relativeUrl : settings.url,
data: settings.data,
context: settings.context || this.requestContext,
beforeSend: settings.beforeSend || undefined
};
//Вызываем
return jquery.ajax(callSettings);
}
И вот как подобная организация нас выручала.
Когда мы писали приложение, описанное в нашей предыдущей статье [2], то реализовали его как SPA. Соответственно, пришлось очень плотно поработать над очисткой памяти, и буквально всё имело конвеер dispose. При этом, возможна ситуация, когда со страницы на сервер шлется запрос, и пользователь внезапно покидает страницу (например, он ткнул не тот пункт меню). View model текущего окна проходит конвеер dispose, пользователь уходит на другую страницу.
И тут с сервера возвращается результат запроса.
Говорить о том, что может случиться в такой ситуации, бессмысленно – случиться может всё, что угодно. Лучшее, что происходило — летела ошибка, и становилось понятно, что что-то не так.
В упоминаемом выше методе callService добавили несколько строчек кода. Кратко их суть в том, что каждый promise мы запоминали. По завершении, либо при вызове метода dispose сервиса — снова забывали.
navThrottleKey: 'Any abracadabra. Guid for example or some string like this.',
callService: function (settings)
{
...
var promise = jquery.ajax(callSettings);
var requestId = ++requestCounter;
this.rejectsOnDispose[requestId] = promise;
promise.always(_.bind(function ()
{
delete this.rejectsOnDispose[requestId];
}, this));
return promise;
}
dispose: function ()
{
for (var indexer in this.rejectsOnDispose)
{
this.rejectsOnDispose[indexer].abort(this.serviceThrottleKey);
delete this.rejectsOnDispose[indexer];
}
this.rejectsOnDispose = null;
this.requestContext = null;
}
Вуаля. Десяток строчек кода решил проблему по всему приложению.
Отметим дополнительным комментарием поле
this.serviceThrottleKey
Это гарантированно уникальная строка, которая позволяет “опознать” описываемую ситуацию и не вызывать обработчики конвеера fail, который неизбежно запускается при вызове метода abort promise-а. То есть мы перехватываем свой же fault и глушим его, чтобы не выводить пользователю на UI сообщение об ошибке.
Бывает такая ситуация, что какой-то вызов к сервису выполняется долго. И, пока он не выполнится, мы не можем давать пользователю что-либо делать (например, когда это загрузка initial данных). При этом хочется показывать пользователю сообщение о том, что необходимо подождать, и заблокировать экран.
А еще бывает, что вызов иногда выполняется долго, а иногда быстро (из кэша). На такие случаи желательно не показывать пользователю появляющееся и тут же пропадающее сообщение, поскольку это утомляет.
Решение лежит все так же в нашем методе callService. Допишем туда еще пяток строчек кода (предыдущие удалим для простоты чтения кода).
callService: function (settings, statusMessage)
{
...
var sid = statusMessage ? statusTracker.trackStatus(statusMessage) : null;
var promise = jquery.ajax(callSettings);
promise.done(_.bind(function ()
{
if (sid !== null)
{
statusTracker.resolveStatus(sid, ETR.ProgressState.Done);
}
}, this)).fail(_.bind(function ()
{
if (sid !== null)
{
statusTracker.resolveStatus(sid, ETR.ProgressState.Fail);
}
}, this));
return promise;
}
Суть проста: если нам передана некая строка statusMessage как параметр, то этот вызов хочет показывать сообщение при вызове.
Показываем так:
statusTracker.trackStatus(statusMessage)
Прячем вот так:
statusTracker.resolveStatus(sid, ETR.ProgressState.Fail);
Объект statusTracker, в свою очередь, работает по следующей логике:
Выглядит это примерно вот так:

У нас возникла ситуация, когда несколько объектов на странице могут попросить одни и те же данные. Ходить на сервер за одними и теми же данными не хочется (как и показывать одинаковые статусы по типу описанных выше). Попробуем что-нибудь придумать.
Решение лежит все в том же методе callService. Суть проста. Определить, что некоторый запрос может вызываться несколько раз, и при этом будет удовлетворительно вернуть один и тот же результат — дело вызывающей стороны. Дополняем метод callService еще десятком строчек:
callService: function (settings, statusMessage, singletKey)
{
…
if (singletKey)
{
var singlet = wcfDispatcherDef.singletsCalls[singletKey];
if (singlet)
{
var singletResolver = jquery.Deferred();
singlet.done(function ()
{
singletResolver.resolveWith(callSettings.context, arguments);
}).fail(function ()
{
singletResolver.rejectWith(callSettings.context, arguments);
});
return singletResolver;
}
}
if (singletKey)
{
wcfDispatcherDef.singletsCalls[singletKey] = promise;
}
…
}
Примечание: подобный подход лучше использовать с осторожностью, поскольку в случае reference типов [5] можно наловить очень много неочевидных проблем. Для решения потенциальных проблем, если позволяет performance, можно использовать jquery.extend [6] с флагом deep или банальный JSON.parse(JSON.stringify()) [7].
Пожалуй, большое описание тут не требуется. Необходимо кэшировать данные на клиенте на определенное время.
Решением стал метод, который лежит в нашем базовом классе сервисов. На вход он принимает функцию того же контракта, что и callService, и параметры кэширования. Возвращает он функцию-обертку, которая перед вызовом сервиса посмотрит в кэш:
wrapWithCache: function (fn, cacheKey, expirationPolicy)
{
return _.wrap(fn, function (initialFn)
{
var cacheResult = cacheService.getCacheValue(cacheKey);
if (cacheResult !== undefined)
{
var q = jquery.Deferred();
_.delay(function (scope)
{
q.resolveWith(scope, [cacheResult]);
}, 0, this.requestContext || this);
return q;
} else
{
return initialFn.apply(this, Array.prototype.slice.call(arguments, 1))
.done(_.bind(function (result)
{
cacheService.setCacheValue(cacheKey, result, expirationPolicy);
}, this));
}
});
}
Примечание 1: на самом деле, использование underscore delay [8] (или нативного setTimeout) на случай использования jquery.ajax не является абсолютно обязательным, поскольку все обработчики для jquery.Deffered, объявленные после его обработки, все равно будут вызваны [8]. Но всё-таки, асинхронную по своей сути модель работы нарушать не стоит, поэтому используем _.delay.
В итоге, если нам хочется обернуть кэшем какой-либо вызов, — вот такой, к примеру:
{
return this.callService({
method: ETR.HttpVerb.POST,
url: this.someServiceUrl + '/getSomeList',
data: request
}).fail(this.commonFaultHandler);
}
то дописываем одну строчку кода в сервис:
this.getSomeList = this.wrapWithCache(this.getSomeList, 'some-cache-key', moment().add(30, 'm'));
Примечание 2: проблемы, оговоренные в предыдущей истории, так же справедливы и для этой: осторожней с reference types.
Случилась у нас как-то история: жило-было приложение, жило хорошо, не жаловалось.
И тут возникло пожелание — некоторые пользователи должны иметь возможность смотреть данные другого пользователя. При этом самому пользователю нам тут показать нечего, а вот если выберет, чьё показать — обязательно покажем. На сервере все это решили просто. Обычный пользователь, который не имеет права что-то смотреть, получает ошибку 403 Forbidden, и итог примерно вот такой:

А для крутых пользователей результат должен быть вот такой:

После выбора пользователю присваивается некий токен и… Да не важно. Рассказ о том, как показать диалог выбора.
По всему приложению… Уже написанному и выпущенному в production…
Спасло то, что на клиенте у нас уже был общий конвеер обработки ошибок. Суть его сводилась к массиву обработчиков, каждый из которых смотрел, может ли он обработать возникшую ошибку. Если один конкретный смог, метод возвращает true, и остальные не вызываются. Если не смог — возвращаем false, и попытка обработать ошибку продолжается.
Примечание: в случае, когда надо обработать совершенно конкретную ошибку в совершенно конкретном месте, вызвать common-овый конвеер обработки — дело обработчика fail конкретного сервиса. Делает он это, если не опознал в ошибке то, что он призван обработать.
Всё что нам понадобилось для решения проблемы — написать метод, который на старте приложения проверял права пользователя. И, в случае чего, вставлял в конвеер еще один обработчик статуса 403, который и покажет заветный диаложек.
Пара часов работы и — готово.
Спасибо всем, кто дочитал. До скорых встреч!
Автор: eastbanctech
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/76838
Ссылки в тексте:
[1] promises: http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt1-theory-and-semantics
[2] один: http://habrahabr.ru/company/eastbanctech/blog/244745/
[3] ноль: http://habrahabr.ru/company/eastbanctech/blog/212197/
[4] setTimeout: http://javascript.ru/setTimeout
[5] reference типов: http://stackoverflow.com/questions/13266616/primitive-value-vs-reference-value
[6] jquery.extend: http://api.jquery.com/jquery.extend/
[7] JSON.parse(JSON.stringify()): http://stackoverflow.com/questions/7914968/cloning-whats-the-fastest-alternative-to-json-parsejson-stringifyx
[8] underscore delay: http://underscorejs.org/#delay
[9] Источник: http://habrahabr.ru/post/245361/
Нажмите здесь для печати.