Публикуется от имени jbubsk. Мы в Тинькофф Банке сто лет боролись с проблемой контекста текущего запроса на сервер. Это момент — когда приходит тикет о том, «что это за ошибка и почему она здесь?» — очень тонкий, и бьет разработчиков по самому больному. В этой статье расскажем, как нам удалось решить эту проблему.
Представьте, что находясь на некой странице приложения, пользователь нажал на кнопку отправки формы или поиска в списке. Время ответа может оказаться большим и неважно, по какой причине: то ли это дорогая и ресурсоемкая операция, то ли интернет медленный, а может быть пользователь — супергерой, и время для него идет слишком медленно.
И вот запрос уже в пути, данные вовсю обрабатываются, но обрабатываются с ошибкой, которую нужно показать пользователю. Клиент интернет-банка, не дожидаясь ответа, переходит на другую страницу в приложении. Он радостно наблюдает выписку по счетам, и вдруг всплывает ошибка: «Дорогой Петр Михалыч, операция была отклонена, а причина этому — луна в Козероге и финансовый кризис». Но Петр Михалыч уже увлечен выпиской и знать не хочет, что не сработал запрос с предыдущей страницы.
Это плохо. Контекст приложения уже другой. Запрос, оставшийся в пендинге, желательно отменить, чтобы избежать сообщения об ошибке на странице, к которой ошибка уже не относится.
С приходом Angular 1.5 мы начали использовать его замечательные components. Они отлично легли в руки разработчиков, код стал лаконичнее, и наша команда с большим удовольствием впитывает все новшества. К сожалению, в любой разработке внедрение чего-то нового не обходится без трудностей.
Ситуация с Петром Михалычем важна независимо от того, есть ли у ангуляра компоненты или нет. Можно, конечно, передавать какую-то ссылку на экземпляр компонента, и из этого же компонента из функции в функцию вниз по цепочке до непосредственно сервиса, который делает запрос на сервер. Если надо, то по этой ссылке просто отменить через этот сервис запрос из пула, в котором он будет храниться. Сам компонент предоставляет удобный хук $onDestroy для таких целей.
Хотя это вполне работающий сценарий, хочется, чтобы в коде было меньше такой вермишели. Цепочка вызовов может быть длинной.
Создание контекста запроса на сервер само по себе непросто. Новое решение должно быть встроено в уже большую рабочую систему. Сильно усложняло задачу то, что нельзя было ломать работающий код. К счастью, вспомнились статьи про контекст во втором ангуляре. После копания в эту сторону, на помощь пришел Zone.js — тот самый, который успешно прикрутили в Angular 2 Многие не понимают, что это за зверь. Даже, несмотря на отсутствие примеров, отличающихся от измерения тайминга, получилось применить контекст зон. Некоторые недопонимания о природе зон все же остались и живут в моей голове.
Вот наше решение проблемы:
Декоратор @cancelable помогает нам на самом верхнем уровне положить вызов метода компонента в зону, привязав зону к экземпляру компонента. Привязка выглядит куда лучше цепочки пробрасываний этого референса из функции в функцию.
Так выглядит декоратор:
zoneService выглядит совсем просто
Наш бизнес-сервис accountsService, который с помощью реализации апи-сервиса получает данные от сервера:
Ну и собственно сам apiService:
В апи-сервисе мы записываем каждый запрос в пул, при это ключом является zoneService.zone.reference, который установился в зону на самом верхнем уровне в цепочке вызовов функций в декораторе.
Запустив функцию в зоне, в любом последующем звене цепочки вызовов мы получим контекст вызова, просто получив зону. Это одним махом решает проблему контекстов для глубоких цепочек вызовов.
Ссылка на экземпляр компонента доступна в необходимом месте. Теперь в методе $onDestroy базового контроллера BaseController мы просто вызываем
и получаем желаемый результат.
Не надо бояться страшных зверей и новых технологий.
Чтобы понять человека, нужно думать как человек
Представьте, что находясь на некой странице приложения, пользователь нажал на кнопку отправки формы или поиска в списке. Время ответа может оказаться большим и неважно, по какой причине: то ли это дорогая и ресурсоемкая операция, то ли интернет медленный, а может быть пользователь — супергерой, и время для него идет слишком медленно.
И вот запрос уже в пути, данные вовсю обрабатываются, но обрабатываются с ошибкой, которую нужно показать пользователю. Клиент интернет-банка, не дожидаясь ответа, переходит на другую страницу в приложении. Он радостно наблюдает выписку по счетам, и вдруг всплывает ошибка: «Дорогой Петр Михалыч, операция была отклонена, а причина этому — луна в Козероге и финансовый кризис». Но Петр Михалыч уже увлечен выпиской и знать не хочет, что не сработал запрос с предыдущей страницы.
Это плохо. Контекст приложения уже другой. Запрос, оставшийся в пендинге, желательно отменить, чтобы избежать сообщения об ошибке на странице, к которой ошибка уже не относится.
Как мы делаем интернет-банк для бизнеса
С приходом Angular 1.5 мы начали использовать его замечательные components. Они отлично легли в руки разработчиков, код стал лаконичнее, и наша команда с большим удовольствием впитывает все новшества. К сожалению, в любой разработке внедрение чего-то нового не обходится без трудностей.
Ситуация с Петром Михалычем важна независимо от того, есть ли у ангуляра компоненты или нет. Можно, конечно, передавать какую-то ссылку на экземпляр компонента, и из этого же компонента из функции в функцию вниз по цепочке до непосредственно сервиса, который делает запрос на сервер. Если надо, то по этой ссылке просто отменить через этот сервис запрос из пула, в котором он будет храниться. Сам компонент предоставляет удобный хук $onDestroy для таких целей.
Хотя это вполне работающий сценарий, хочется, чтобы в коде было меньше такой вермишели. Цепочка вызовов может быть длинной.
Создание контекста запроса на сервер само по себе непросто. Новое решение должно быть встроено в уже большую рабочую систему. Сильно усложняло задачу то, что нельзя было ломать работающий код. К счастью, вспомнились статьи про контекст во втором ангуляре. После копания в эту сторону, на помощь пришел Zone.js — тот самый, который успешно прикрутили в Angular 2 Многие не понимают, что это за зверь. Даже, несмотря на отсутствие примеров, отличающихся от измерения тайминга, получилось применить контекст зон. Некоторые недопонимания о природе зон все же остались и живут в моей голове.
Вот наше решение проблемы:
class AccountsController extends BaseController {
...
constructor(private accountsService: AccountsService) {
super()
}
@cancelable
getAccounts() {
this.accountsService.list(...).then(accounts => this.accounts = accounts);
}
}
class BaseController implements ng.IComponentController {
public serializeId: string;
constructor() {
this.serializeId = `${Date.now()}_${Math.random()}`;
}
$onDestroy() {
const injector: ng.auto.IInjectorService = angular.element(window.document.body).injector();
const apiService: ApiService = <ApiService>injector.get('ApiService');
apiService.cancelRequests(this.serializeId);
}
}
Декоратор @cancelable помогает нам на самом верхнем уровне положить вызов метода компонента в зону, привязав зону к экземпляру компонента. Привязка выглядит куда лучше цепочки пробрасываний этого референса из функции в функцию.
Так выглядит декоратор:
export function cancelable(targetInstance: any, key: string, targetFunction: any) {
return {
value: function (...args: any[]) {
let result;
zoneService.zone.current.run(() => {
zoneService.zone.reference = this.serializeId;
result = targetFunction.value.apply(this, args);
});
return result;
}
};
}
zoneService выглядит совсем просто
export class ZoneService {
get zone() {
return (<any>window).Zone;
}
}
export default new ZoneService();
Наш бизнес-сервис accountsService, который с помощью реализации апи-сервиса получает данные от сервера:
class AccountsService {
...
constructor(private apiService: ApiService) {
}
list(...): ICancelablePromise<AccountsDto> {
return this.apiService.makeGetRequest<AccountsDto>(...);
}
...
}
Ну и собственно сам apiService:
class ApiService {
private requestsPool: Map<string, ICancelablePromise<any>[]> = new Map<string, ICancelablePromise<any>[]>();
...
public makeRequest<T>(...): ICancelablePromise<T> {
const requestService = new HttpRequestService<T>(...);
this.httpRequestConfigService.getConfig(...)
.then(httpRequestConfig => requestService.doHttpRequest(httpRequestConfig));
this.addRequestToPool(zoneService.zone.reference, requestService.requestDeferred.promise);
return requestService.requestDeferred.promise;
}
public cancelRequests(requestKey: string) {
const requests = this.requestsPool.get(requestKey);
if (requests) {
_.forEach(requests, request => request.cancel());
this.requestsPool.delete(requestKey);
}
}
...
private addRequestToPool(key: string, value: ICancelablePromise<any>) {
const requests = this.requestsPool.get(key) || [];
requests.push(value);
this.requestsPool.set(key, requests);
}
}
В апи-сервисе мы записываем каждый запрос в пул, при это ключом является zoneService.zone.reference, который установился в зону на самом верхнем уровне в цепочке вызовов функций в декораторе.
...
zoneService.zone.current.run(() => {
zoneService.zone.reference = this.serializeId;
result = targetFunction.value.apply(this, args);
});
...
Запустив функцию в зоне, в любом последующем звене цепочки вызовов мы получим контекст вызова, просто получив зону. Это одним махом решает проблему контекстов для глубоких цепочек вызовов.
Ссылка на экземпляр компонента доступна в необходимом месте. Теперь в методе $onDestroy базового контроллера BaseController мы просто вызываем
...
apiService.cancelRequests(this.serializeId);
...
и получаем желаемый результат.
Не надо бояться страшных зверей и новых технологий.