Pull to refresh
289.48
TINKOFF
IT’s Tinkoff — просто о сложном

Четкий пацан Zone.js

Reading time 4 min
Views 20K
Публикуется от имени jbubsk. Мы в Тинькофф Банке сто лет боролись с проблемой контекста текущего запроса на сервер. Это момент — когда приходит тикет о том, «что это за ошибка и почему она здесь?» — очень тонкий, и бьет разработчиков по самому больному. В этой статье расскажем, как нам удалось решить эту проблему.


Чтобы понять человека, нужно думать как человек



Представьте, что находясь на некой странице приложения, пользователь нажал на кнопку отправки формы или поиска в списке. Время ответа может оказаться большим и неважно, по какой причине: то ли это дорогая и ресурсоемкая операция, то ли интернет медленный, а может быть пользователь — супергерой, и время для него идет слишком медленно.
И вот запрос уже в пути, данные вовсю обрабатываются, но обрабатываются с ошибкой, которую нужно показать пользователю. Клиент интернет-банка, не дожидаясь ответа, переходит на другую страницу в приложении. Он радостно наблюдает выписку по счетам, и вдруг всплывает ошибка: «Дорогой Петр Михалыч, операция была отклонена, а причина этому — луна в Козероге и финансовый кризис». Но Петр Михалыч уже увлечен выпиской и знать не хочет, что не сработал запрос с предыдущей страницы.
Это плохо. Контекст приложения уже другой. Запрос, оставшийся в пендинге, желательно отменить, чтобы избежать сообщения об ошибке на странице, к которой ошибка уже не относится.

Как мы делаем интернет-банк для бизнеса



С приходом 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);
...


и получаем желаемый результат.

Не надо бояться страшных зверей и новых технологий.
Tags:
Hubs:
+10
Comments 14
Comments Comments 14

Articles

Information

Website
www.tinkoff.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия