Beobachtbare Dienste in Angular

Hallo allerseits, mein Name ist Vladimir. Ich beschäftige mich mit der Front-End-Entwicklung in Tinkoff.ru.


In Angular sind wir es gewohnt, Dienste zu verwenden, um Daten innerhalb einer Anwendung zu übertragen oder Geschäftslogik zu kapseln. RxJS eignet sich hervorragend zum Verwalten von asynchronen Threads.


Angular in Kombination mit RxJS ermöglicht es Ihnen, in einem deklarativen Stil zu schreiben, kurz und klar. Aber manchmal stoßen wir auf Bibliotheken oder APIs von Drittanbietern, die Rückrufe und Versprechen verwenden, und zwingen uns daher, vom üblichen Stil zurückzutreten und zwingend zu schreiben.


Der Zweck des Artikels besteht darin, am Beispiel ähnlicher APIs zu zeigen, wie sie mit RxJS einfach in Observable-Dienste eingeschlossen werden können. Dies wird dazu beitragen, die Benutzerfreundlichkeit in Angular zu erreichen. Beginnen wir mit der Geolocation-API.



Geolocation-API


Mit der Geolocation-API kann der Benutzer der Webanwendung seinen Standort mitteilen, wenn der Benutzer dem zustimmt. Aus Datenschutzgründen wird der Benutzer um Erlaubnis gebeten, Standortinformationen anzugeben.

Das Folgende ist ein Beispiel für die grundlegende Verwendung der nativen Geolocation-API.


, 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