【问题标题】:Angular Service is undefined when trying to implement a timer尝试实现计时器时未定义角度服务
【发布时间】:2020-10-15 14:24:38
【问题描述】:

我正在尝试完成以下操作:对于我的网络应用程序,我尝试实现一个计时器,当用户显示活动(意味着触发点击、keydown 或滚动事件)时,它会自动重置回 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:&lt;p&gt;{{this.DataService.timeLeft}}&lt;/p&gt;

我的错误:

无。

杂项。控制台输出:

console.log 打印一次 Juup,然后打印出 timeLeft 的 undefined。每次点击都会发生这种情况。

【问题讨论】:

    标签: angular typescript timer rxjs clearinterval


    【解决方案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 - 高阶映射运算符用于在源 observable(此处为 merge)发出时取消当前的内部 observable(此处为 timer)。
    • timer - 用于每秒发出一个值。
    • map - 将值从 timer 转换为我们的倒数计时器值。
    • finalize - 仅在 timer 完成时触发。换句话说,只要源 observables 在 60 秒内持续发射,它就不会被触发。所以你可以在这里包含路由机制。

    更新 - finalize 无法正常工作

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

    • tap - 只有在 timer 完成时才会触发 complete 回调。换句话说,只要源 observables 在 60 秒内持续发射,它就不会被触发。因此,您可以在此处包含路由机制。

    更新:通过身份验证服务使用倒计时

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

    关于你在评论区提出的问题

    1. 将countDown$ 变量定义为any 类型是否是一种好习惯?

    简而言之,没有。你可以使用 Observable&lt;number&gt; 代替,因为它保存了从 observable 发出的数字。

    1. 此外,我还收到了来自 tap 操作员的以下弃用警告: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

    【讨论】:

    • 为什么两个相同的 mouseup 事件发射器会合并?
    • @Munchkin:应该是mouseup 和mousemove。我忽略了它。我已经编辑了答案。这里的事件仅用于说明。
    • @Munchkin:请稍等,倒计时结束时不会触发finalize。
    • @Munchkin:我已经更新了答案,将finalize 替换为tap 的complete 回调。请注意区别。
    • 将 countDown$ 变量定义为任何类型是否是一种好习惯?
    猜你喜欢
    • 1970-01-01
    • 2021-08-30
    • 1970-01-01
    • 2016-03-20
    • 1970-01-01
    • 2012-11-02
    • 1970-01-01
    • 1970-01-01
    • 2021-02-20
    相关资源
    最近更新 更多