Angular Service не определен при попытке реализовать таймер

Я пытаюсь выполнить следующее: для своего веб-приложения я попытался реализовать таймер, который автоматически сбрасывается до 60 с, когда пользователь проявляет активность (то есть запускает событие щелчка, нажатия клавиши или прокрутки). Я использовал Observables для потоков событий. Все это происходит ПОСЛЕ, когда вы входите в мое веб-приложение.

Если таймер истекает (переход от 60 с к 0), пользователь должен быть просто перенаправлен на внутренний URL-адрес, который удаляет файл cookie и перенаправляет обратно на экран входа в систему внешнего интерфейса. Теперь все хорошо и денди, тайм-аут работает, но когда я что-то нажимаю, прокручиваю или нажимаю что-то на клавиатуре, ничего не происходит. Я также хотел бы иметь поток событий, который обнаруживает движение мыши, как мне это сделать с RxJS?

Мой код:

Служба данных:

// login timer
 public timeLeft = 60;
 public interval: number;

  constructor(private http: HttpClient, public datepipe: DatePipe, private cookieService: CookieService) {
  }

  public startTimer() {
    this.interval = setInterval(() => {
      if(this.timeLeft > 0) {
        this.timeLeft--;
      } else {
        // FIXME HTTPS implementation
        window.location.href = 'http://example.com:8081/logout';
        clearInterval(this.interval);
        this.timeLeft = 60;
      }
    }, 1000);
  }

  public resetTimer() {
    console.log('Juup');
    clearInterval(this.interval);
    console.log(this.timeLeft);
    //this.timeLeft = 60;
    //this.startTimer();
    /* let time;
    clearTimeout(time);
    time = setTimeout(this.logout, 3000); */
  }

Логика входа:

return this.http.post<any>(`${environment.apiUrl}/api/login`, { email, password }, {withCredentials: true})
        .pipe(map(user => {
// login logic here
// if everything is ok then
this.DataService.startTimer();

app.component.ts:

constructor(public authenticationService: AuthenticationService, public DataService: GlobalDataService,
              private router: Router) {
    const source1$ = fromEvent(document, 'click');
    const source2$ = fromEvent(document, 'keydown');
    const source3$ = fromEvent(document, 'scroll');
    const sources$ = merge (
    source1$,
    source2$,
    source3$
  );
// map to string with given event timestamp
    const example = sources$.pipe(map(event => `Event time: ${event.timeStamp}`));
// output (example): 'Event time: 7276.390000000001'
//
// const subscribe = sources$.subscribe(val => console.log(val));
    const subscribe = sources$.subscribe(this.DataService.resetTimer);

interval: number;
subscribeTimer: any;

observableTimer() {
    const source = timer(1000, 2000);
    const abc = source.subscribe(val => {
      console.log(val, '-');
      this.subscribeTimer = this.DataService.timeLeft - val;
    });
  }

Мой app.component.html: <p>{{this.DataService.timeLeft}}</p>

Мои ошибки:

Никто.

Разное вывод консоли:

console.log выводит Juup один раз, а затем выводит undefined для timeLeft. Это происходит каждый раз, когда я нажимаю.


person Munchkin    schedule 15.10.2020    source источник


Ответы (1)


Я не смотрел на вашу конкретную проблему. Но вот очень простая реализация таймера обратного отсчета, который сбрасывается каждый раз, когда пользователь щелкает или перемещает мышь в любом месте приложения.

import { Component, OnInit } from "@angular/core";

import { timer, Observable, fromEvent, merge, Subject } from "rxjs";
import { startWith, switchMap, finalize, take, map } from "rxjs/operators";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
  countDown$: Observable<any>;

  ngOnInit() {
    this.countDown$ = merge(
      fromEvent(document, "mouseup"),
      fromEvent(document, "mousemove")
    ).pipe(
      startWith(false),
      switchMap(_ => {
        let counter = 61;
        return timer(0, 1000).pipe(
          take(counter),
          map(_ => --counter),
          tap(null, null, () => {
            console.log("countdown complete");
            // redirect to login page
          })
        );
      })
    );
  }
}

Рабочий пример: Stackblitz

Рабочий пример: Stackblitz

Авария

  • fromEvent — используется для захвата событий mousedown и mouseup. Вы можете настроить его для определенной части экрана, используя эталонные переменные шаблона Angular, или добавить/удалить дополнительные события.
  • merge — используется для объединения нескольких событий в одно событие
  • startWith — Используется для генерации случайной переменной перед генерацией любого события. Используется для запуска обратного отсчета при запуске приложения.
  • switchMap — оператор отображения более высокого порядка, используемый для отмены текущего внутреннего наблюдаемого (здесь timer), когда испускается исходный наблюдаемый (здесь merge).
  • timer — Используется для создания значения каждую секунду.
  • map — преобразовать значение из timer в значение нашего таймера обратного отсчета.
  • finalize — будет запущено только после завершения timer. Другими словами, пока исходные наблюдаемые объекты продолжают излучать в течение 60 секунд, они не будут запущены. Таким образом, вы можете включить сюда механизм маршрутизации.

Обновление – finalize не работает должным образом

Похоже, что каждое событие в merge, возможно, просачивается в оператор finalize, поскольку каждое из них по отдельности завершается до изменения наблюдаемого из switchMap. Я заменил его полным блоком оператора tap, который работает, как и ожидалось. Приведенное выше объяснение для finalize должно по-прежнему работать, но вместо этого заменено на tap.

  • tap — обратный вызов complete будет запущен только после завершения timer. Другими словами, пока исходные наблюдаемые объекты продолжают излучать в течение 60 секунд, они не будут запущены. Таким образом, вы можете включить сюда механизм маршрутизации.

Обновление: используйте обратный отсчет со службой аутентификации

Я полностью обновил иллюстрацию, чтобы она работала в тандеме со службой аутентификации, которая имитирует HTTP-вызовы для действий входа и выхода.

По вашим вопросам в разделе комментариев

  1. Является ли хорошей практикой определение переменной countDown$ как типа any?

Короче говоря, нет. Вместо этого вы можете использовать Observable<number>, так как он содержит число, испускаемое из наблюдаемого.

  1. Также я получаю следующее предупреждение об устаревании от оператора крана: tap is deprecated: Use an observer instead of a complete callback (deprecation) Как исправить устаревание?

tap не считается устаревшим. Скорее всего, речь идет о перегрузке функции, которая устарела. Вместо этого вы можете использовать объект со свойствами next, error и complete.

  1. Кстати, мне нужно запускать таймер только после входа в систему, как мне это сделать? В моем коде я просто использую вызов startTimer() в службе аутентификации.

Надеюсь, обновленной иллюстрации будет достаточно.

app.component.ts

import { Component, OnDestroy } from "@angular/core";

import { fromEvent, merge, Subject } from "rxjs";
import { switchMap, takeUntil } from "rxjs/operators";

import { AuthenticationService } from "./authentication.service";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnDestroy {
  closed$ = new Subject<any>();
  countDown: number;

  constructor(private authService: AuthenticationService) {}

  login() {
    this.authService
      .login()
      .pipe(
        switchMap(_ =>
          merge(
            fromEvent(document, "mouseup"), // <-- replace these with user events to reset timer
            fromEvent(document, "mousemove")
          )
        ),
        this.authService.countdownTimer(),
        takeUntil(this.closed$)
      )
      .subscribe(countDown => (this.countDown = countDown));
  }

  logout() {
    this.authService
      .logout()
      .pipe(takeUntil(this.closed$))
      .subscribe();
  }

  ngOnDestroy() {
    this.closed$.next();
  }
}

app.component.html

<ng-container *ngIf="(authService.loginStatus$ | async); else loggedOut">
  <p>User is logged in. Automatic logout in: {{ countDown }}s</p>
  <br />
  <button (mouseup)="logout()">Logout</button>
</ng-container>
<ng-template #loggedOut>
  <p>User is logged out.</p>
  <br />
  <button (mouseup)="login()">Login</button>
</ng-template>

authentication.service.ts

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

import { timer, Observable, Subject, BehaviorSubject } from "rxjs";
import {
  tap,
  startWith,
  switchMap,
  takeUntil,
  take,
  map
} from "rxjs/operators";

export const LOGIN_TIME = 6;

@Injectable()
export class AuthenticationService {
  public loginStatusSrc = new BehaviorSubject<boolean>(false);
  public stopTimerSrc = new Subject<any>();
  public loginStatus$ = this.loginStatusSrc.asObservable();
  public stopTimer$ = this.stopTimerSrc.asObservable();

  constructor(private http: HttpClient) {}

  login(): Observable<any> {
    // simulate a login HTTP call
    return this.http.get("https://jsonplaceholder.typicode.com/users/1").pipe(
      tap({
        next: () => this.loginStatusSrc.next(true),
        error: () => this.loginStatusSrc.next(false)
      })
    );
  }

  logout() {
    // simulate a logout HTTP call
    return this.http.get("https://jsonplaceholder.typicode.com/users/1").pipe(
      tap({
        next: () => {
          this.loginStatusSrc.next(false); // <-- hide timer
          this.stopTimerSrc.next(); // <-- stop timer running in background
        },
        error: () => this.loginStatusSrc.next(false)
      })
    );
  }

  countdownTimer() {
    return <T>(source: Observable<T>) => {
      return source.pipe(
        startWith(null),
        switchMap(_ => {
          let counter = LOGIN_TIME;
          return timer(0, 1000).pipe(
            take(counter),
            map(_ => --counter),
            tap({
              next: null,
              error: null,
              complete: () => {
                this.stopTimerSrc.next(); // <-- stop timer in background
                this.loginStatusSrc.next(false);
                console.log("Countdown complete. Rerouting to login page...");
                // redirect to login page
              }
            })
          );
        }),
        takeUntil(this.stopTimer$)
      );
    };
  }
}

Вот руководство, которое я использовал для создания пользовательского оператора RxJS countdownTimer(): https://netbasal.com/creating-custom-operators-in-rxjs-32f052d69457

Рабочий пример: Stackblitz

person Michael D    schedule 15.10.2020
comment
Почему сливаются два идентичных эмиттера событий от mouseup? - person Munchkin; 15.10.2020
comment
@Munchkin: Это должны были быть mouseup и mousemove. Я проглядел это. Я отредактировал ответ. События здесь только для иллюстрации. - person Michael D; 15.10.2020
comment
@Munchkin: Подождите, пожалуйста, finalize не срабатывает в конце обратного отсчета. - person Michael D; 15.10.2020
comment
@Munchkin: я обновил ответ, заменив finalize на обратный вызов tap complete. Обратите внимание на разницу. - person Michael D; 15.10.2020
comment
Является ли хорошей практикой определение переменной countDown$ любого типа? - person Munchkin; 16.10.2020
comment
Также я получаю следующее предупреждение от оператора крана об устаревании: tap is deprecated: Use an observer instead of a complete callback (deprecation) Как исправить устаревание? - person Munchkin; 16.10.2020
comment
Кстати, мне нужно запускать таймер только после входа в систему, как мне это сделать? В моем коде я просто использую вызов startTimer() в службе аутентификации. - person Munchkin; 16.10.2020
comment
@Munchkin: я обновил ответ вместе с ответами на ваши вопросы. Пожалуйста, посмотрите, работает ли это для вас. - person Michael D; 16.10.2020