Angular 尝试实现计时器时服务未定义

Angular Service is undefined when trying to implement a timer

我正在尝试完成以下操作:对于我的网络应用程序,我尝试实现一个计时器,当用户显示 activity(意味着触发点击、按下或滚动事件时,该计时器会自动重置回 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>

我的错误:

None.

杂项。控制台输出:

console.log 打印一次 Juup,然后为 timeLeft 打印出 undefined。每次我点击它都会发生。

我没有调查你的具体问题。但这是一个非常基本的倒计时计时器实现,每次用户在应用程序中的任何位置单击或移动鼠标时都会重置。

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 - 用于捕获 mousedownmouseup 事件。您可以使用 Angular 模板参考变量或 add/remove 附加事件将其调整到屏幕的特定部分。
  • merge - 用于将多个事件合并为一个事件
  • startWith - 用于在任何事件发出之前发出一个随机变量。用于在应用启动时启动倒计时。
  • switchMap - 用于在源可观察对象(merge 此处)发出时取消当前内部可观察对象(此处的 timer)的高阶映射运算符。
  • timer - 用于每秒发出一个值。
  • map - 将值从 timer 转换为我们的倒数计时器值。
  • finalize - 仅在 timer 完成时触发。换句话说,只要 source observables 在 60 秒内持续发射,它就不会被触发。所以你可以在这里包含路由机制。

更新 - finalize 未按预期工作

merge 中的每个事件似乎都可能渗透到 finalize 运算符中,因为它们中的每一个似乎都在 switchMap 的修改后的可观察对象之前完成。我已将其替换为 tap 运算符的完整块,该块按预期工作。上面对 finalize 的解释应该仍然有效,但用 tap 代替

  • tap - 它的 complete 回调只会在 timer 完成时触发。换句话说,只要 source observables 在 60 秒内持续发射,它就不会被触发。所以你可以在这里包含路由机制。

更新:将倒计时与身份验证服务结合使用

我已经完全更新了插图,以便与模拟 HTTP 登录和注销操作调用的身份验证服务协同工作。

关于评论区的问题

  1. countDown$ 变量定义为 any 类型是一个好习惯吗?

简而言之,没有。您可以改用 Observable<number>,因为它包含从可观察对象发出的数字。

  1. 我还从 tap 操作员处收到以下弃用警告:tap is deprecated: Use an observer instead of a complete callback (deprecation) 如何解决弃用问题?

tap 未弃用。很可能是关于已弃用的函数重载。您可以改用具有 nexterrorcomplete 属性的对象。

  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