Services observables à Angular

Bonjour à tous, mon nom est Vladimir. Je suis engagé dans le développement front-end sur Tinkoff.ru.


Dans Angular, nous sommes habitués à utiliser des services pour transférer des données à l'intérieur d'une application ou pour encapsuler la logique métier. RxJS est idéal pour gérer les threads asynchrones.


Angular en combinaison avec RxJS vous permet d'écrire dans un style déclaratif, court et clair. Mais parfois, nous rencontrons des bibliothèques ou des API tierces qui utilisent des rappels, des promesses, nous poussant ainsi à prendre du recul par rapport au style habituel et à écrire impérativement.


Le but de l'article est de montrer par l'exemple d'API similaires comment en utilisant RxJS ils peuvent être facilement enveloppés dans des services observables. Cela aidera à atteindre l'utilisabilité dans Angular. Commençons par l'API de géolocalisation.



API de géolocalisation


L'API de géolocalisation permet à l'utilisateur de fournir son emplacement à l'application Web si l'utilisateur y consent. Pour des raisons de confidentialité, il sera demandé à l'utilisateur l'autorisation de fournir des informations de localisation.

Voici un exemple d'utilisation de base de l'API de géolocalisation native.


, Geolocation :


if('geolocation' in navigator) {
 /* geolocation is available */ 
} else {
 /* geolocation IS NOT available */ 
}

getCurrentPosition(), . watchPosition(), , . , .


...
success(position) {
 doSomething(position.coords.latitude, position.coords.longitude);
} 

error() {
 alert('Sorry, no position available.');
} 

watch() {
 this.watchID = navigator.geolocation.watchPosition(this.success, this.error);
}

stopWatch() {
  navigator.geolocation.clearWatch(this.watchID);
}
...

- , clearWatch() id . . . , RxJS Observable.


Geolocation, . . NAVIGATOR @ng-web-apis/common.


export const GEOLOCATION = new InjectionToken<Geolocation>(
   'An abstraction over window.navigator.geolocation object',
   {
       factory: () => inject(NAVIGATOR).geolocation,
   },
);

, Geolocation API:


export const GEOLOCATION_SUPPORT = new InjectionToken<boolean>(
   'Is Geolocation API supported?',
   {
       factory: () => !!inject(GEOLOCATION),
   },
);

PositionOptions:


import {InjectionToken} from '@angular/core';

export const POSITION_OPTIONS = new InjectionToken<PositionOptions>(
   'Token for an additional position options',
   {factory: () => ({})},
);

! Observable .


Observable, , , . , . Subscriber, , next(), complete() error(). , :


@Injectable({
   providedIn: 'root',
})
export class GeolocationService extends Observable<Position> {
   constructor(
       @Inject(GEOLOCATION) geolocationRef: Geolocation) {

       super(subscriber => {

           geolocationRef.watchPosition(
               position => subscriber.next(position),
               positionError => subscriber.error(positionError),
           );
       });
   }
}

:


@Injectable({
   providedIn: 'root',
})
export class GeolocationService extends Observable<Position> {
  constructor(
  @Inject(GEOLOCATION) geolocationRef: Geolocation,
  @Inject(GEOLOCATION_SUPPORT) geolocationSupported: boolean,
  @Inject(POSITION_OPTIONS) positionOptions: PositionOptions,
) {
  super(subscriber => {
    if (!geolocationSupported) {
      subscriber.error('Geolocation is not supported in your browser');
    }

    geolocationRef.watchPosition(
      position => subscriber.next(position),
      positionError => subscriber.error(positionError),
      positionOptions,
    );
   })
  }
}

! Observable, pipe(). RxJS- . , , RxJS- shareReplay().
share()? , Geolocation API getCurrentPosition() watchPosition() Timeout expired. shareReplay() , , . {bufferSize: 1, refCount: true}, . clearWatch()-, , RxJS- finalize():


@Injectable({
   providedIn: 'root',
})
export class GeolocationService extends Observable<Position> {
   constructor(
       @Inject(GEOLOCATION) geolocationRef: Geolocation,
       @Inject(GEOLOCATION_SUPPORT) geolocationSupported: boolean,
       @Inject(POSITION_OPTIONS)
       positionOptions: PositionOptions,
   ) {
       let watchPositionId = 0;

       super(subscriber => {
           if (!geolocationSupported) {
               subscriber.error('Geolocation is not supported in your browser');
           }

           watchPositionId = geolocationRef.watchPosition(
               position => subscriber.next(position),
               positionError => subscriber.error(positionError),
               positionOptions,
           );
       });

       return this.pipe(
           finalize(() => geolocationRef.clearWatch(watchPositionId)),
           shareReplay({bufferSize: 1, refCount: true}),
       );
   }
}

! , :


...
constructor(@Inject(GeolocationService) private readonly position$: Observable<Position>) {}
...
   position$.pipe(take(1)).subscribe(position => doSomethingWithPosition(position));
...

, async pipe. @angular/google-maps .


<app-map
       [position]="position$ | async"
></app-map>

RxJS .


Geolocation API. . npm.


ResizeObserver API , .


ResizeObserver API


API Resize Observer , .

Geolocation API, ResizeObserver API . Observable-. , . , , :


@Injectable()
export class ResizeObserverService extends Observable<
   ReadonlyArray<ResizeObserverEntry>
> {
   constructor(
       @Inject(ElementRef) {nativeElement}: ElementRef<Element>,
       @Inject(NgZone) ngZone: NgZone,
       @Inject(RESIZE_OBSERVER_SUPPORT) support: boolean,
       @Inject(RESIZE_OPTION_BOX) box: ResizeObserverOptions['box'],
   ) {
       let observer: ResizeObserver;

       super(subscriber => {
           if (!support) {
               subscriber.error('ResizeObserver is not supported in your browser');
           }

           observer = new ResizeObserver(entries => {
               ngZone.run(() => {
                   subscriber.next(entries);
               });
           });
           observer.observe(nativeElement, {box});
       });

       return this.pipe(
           finalize(() => observer.disconnect()),
           share(),
       );
   }
}

ResizeObserver. next() ngZone() .


Geolocation- , ElementRef. , observe(). . Dependency Injection , .


, . . Output(), :


@Directive({
   selector: '[waResizeObserver]',
   providers: [
       ResizeObserverService,
       {
           provide: RESIZE_OPTION_BOX,
           deps: [[new Attribute('waResizeBox')]],
           useFactory: boxFactory,
       },
   ],
})
export class ResizeObserverDirective {
   @Output()
   readonly waResizeObserver: Observable<ResizeObserverEntry[]>;

   constructor(
       @Inject(ResizeObserverService)
       entries$: Observable<ResizeObserverEntry[]>
   ) {
       this.waResizeObserver = entries$;
   }
}

, RESIZE_OPTION_BOX . ReziseObserver .


. , :


<div
       waResizeBox="content-box"
       (waResizeObserver)="onResize($event)"
   >
       Resizable box
</div>

...
   onResize(entry: ResizeObserverEntry[]) {
       // do something with entry
   }
...

open-source-, npm. «».



RxJS Dependency Injection API, . . , — , @ng-web-apis/midi. .


, .


, , Web APIs for Angular. — API Angular-. , , , Payment Request API Intersection Observer, — .


All Articles