Angular NgRx-继续轮询仅称为第一次的服务的效果

时间:2019-10-17 06:35:39

标签: angular rxjs ngrx ngrx-effects

我有一个应用程序,我刚刚在其中添加了NgRX,我希望使用效果来打开和关闭轮询。

示例大纲

我遵循了this post,这似乎是一种不错的方法。我有一个here的简化示例,其中大部分代码位于app.effects.ts中。

与示例类似,除了使用新的startPolling$工厂方法外,我还具有stopPolling$continuePolling$createEffect的效果。

此外,我将delay(2000)移动到takeWhile()上方,因为我发现如果服务调用引发错误,则catchError(err => of(appActions.getDataFail(err)))会导致效果持续不断快速循环而没有延迟。

“开始和停止”按钮调度轮询的开始和停止...

public start() {
    console.log('dispatching start');
    this.store.dispatch(appActions.startPolling());
  }

  public stop() {
    console.log('dispatching stop');
    this.store.dispatch(appActions.stopPolling());
  }

我的问题

我有一些控制台日志,所以我们可以看到发生了什么事。

当我们单击开始按钮(恰好是第一次时间)时,我可以看到轮询开始,然后按预期进行。例如,我可以一遍又一遍地看到以下内容...

dispatching start
app effect started polling
app.service.getData
app effect continue polling
app.service.getData
app effect continue polling
app.service.getData
app effect continue polling

完美。

当我停下来时,我会看到

dispatching stop
app effect stop polling

也正确。

现在,问题是我尝试重新启动时。如果我现在再次单击开始按钮,那么我看到的只是初始开始轮询效果...

dispatching start
app effect started polling
app.service.getData

,并且不再调用 continuePolling$中的代码,因此我没有轮询。

有人知道为什么这种效果没有在秒时间内触发吗?我只是不知道为什么会这样。

提前感谢您提供任何信息。

[UPDATE1]

我认为也许我的问题是,一旦isPollingActive设置为false,并且takeWhile(() => this.isPollingActive),“停止”,则可观察对象将不再处于活动状态,即continuePolling$已完成,因此永远不会重新启动?

假设如此,我尝试了以下操作,其中有2个不同的变量,一个用于“暂停”轮询(例如,如果我在离线模式下检测到该应用程序),另一个用于取消(即,当用户退出导航时)组件)。

所以,我的整个效果变成了...

    @Injectable()
    export class AppEffects {
      private isPollingCancelled: boolean;
      private isPollingPaused: boolean;

      constructor(
        private actions$: Actions,
        private store: Store<AppState>,
        private appDataService: AppDataService
      ) { }

      public startPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.startPolling),
        tap(_ => console.log('app effect started polling')),
        tap(() => {
          this.isPollingCancelled = false;
          this.isPollingPaused = false;
        }),        
          mergeMap(() =>
            this.appDataService.getData()
              .pipe(                        
                switchMap(data => {              
                  return [appActions.getDataSuccess(data)
                  ];
                  }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ));

         public pausePolling$ = createEffect(() => this.actions$.pipe(
            ofType(appActions.pausePolling),
            tap(_ => this.isPollingPaused = true),
            tap(_ => console.log('app effect pause polling')),       
         ));

      public cancelPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.cancelPolling),
        tap(_ => this.isPollingCancelled = true),
        tap(_ => console.log('app effect cancel polling')),
      ));

        public continuePolling$ = createEffect(() => this.actions$.pipe(
          ofType(appActions.getDataSuccess, appActions.getDataFail),    
          tap(data => console.log('app effect continue polling')),  
          takeWhile(() => !this.isPollingCancelled),    
          delay(3000),  

          mergeMap(() =>
            this.appDataService.getData()
              .pipe(   
                delay(3000),  
                tap(data => console.log('app effect continue polling - inner loop')),  
                takeWhile(() => !this.isPollingPaused), // check again incase this has been unset since delay 
                switchMap(data => {              
                  return [appActions.getDataSuccess(data)
                  ];
                  }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ));    
    } 

我不建议运行上面的命令,因为当我随后分发pause polling action时,效果似乎陷入了无休止的循环,我不得不通过任务管理器终止浏览器。

我不知道为什么会这样,但是我似乎比以前更远离解决方案。

再次提供任何帮助,

[UPDATE2]

我注意到我没有从暂停和取消效果中返回任何动作。

所以我们更新了它们,我们关注...

 public pausePolling$ = createEffect(() => this.actions$.pipe(
    ofType(appActions.pausePolling),
    tap(_ => this.isPollingPaused = true),
    tap(_ => console.log('app effect pause polling')),
    map(_ => appActions.pausePollingSuccess())
  ));

  public cancelPolling$ = createEffect(() => this.actions$.pipe(
    ofType(appActions.cancelPolling),
    tap(_ => {
      this.isPollingCancelled = true;
      this.isPollingPaused = true;
    }),
    tap(_ => console.log('app effect cancel polling')),
    map(_ => appActions.cancelPollingSuccess())
  ));

现在暂停似乎可以正常工作,但是当我调度appActions.cancelPolling时,我再次看到app effect cancel polling的无限循环被记录到控制台。

[UPDATE3]

我发现了为什么得到无限循环以及如何停止它。根据文档here,我可以添加dispatch:false ...

    public cancelPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.cancelPolling),
        tap(_ => {
          this.isPollingCancelled = true;
          this.isPollingPaused = true;
        }),
        tap(_ => console.log('app effect cancel polling')),
      ), { dispatch: false }); // <------ add this

这似乎解决了我的无限循环。

我现在唯一的任务是能够确定如何启动,停止和重新启动轮询,以处理对appDataService.getData()的成功调用以及异常情况。

我可以使它适用于任何一个(取决于我放置延迟和时间的地方),但不能同时适用于两者

[UPDATE4]

我有最新的代码here

按原样运行它,我使getData成功,而且令人惊讶的是,无论是pause还是stop操作都将停止它并允许它重新启动。.我很惊讶 stop 操作允许它重新启动,就像我假设takeWhile(() => !this.isPollingCancelled),会取消效果一样。

此外,如果将true传递给getData,这将导致它可观察到错误。轮询继续进行(根据需要,即,即使出现错误也要重试),但是一旦我们现在分派暂停操作,它就不会停止轮询,而是分派停止,它确实会停止,但随后不会重新启动。我赢不了。

[UPDATE 5]

我想也许是因为取消了持续轮询效果,所以每次都可以重新创建它,如下所示。

    import { Injectable, OnInit, OnDestroy } from '@angular/core';
    import { createEffect, Actions, ofType } from '@ngrx/effects';
    import { select, Store } from '@ngrx/store';
    import { mergeMap, map, catchError, takeWhile, delay, tap, switchMap } from 'rxjs/operators';
    import { AppState } from './app.state';
    import { Observable, of } from 'rxjs';
    import { AppDataService } from '../app-data.service';
    import * as appActions from './app.actions';

    @Injectable()
    export class AppEffects {
      private isPollingCancelled: boolean;
      private isPollingPaused: boolean;

      constructor(
        private actions$: Actions,
        private store: Store<AppState>,
        private appDataService: AppDataService
      ) { }

      public startPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.startPolling),
        tap(_ => console.log('app effect started polling')),
        tap(() => {
          this.isPollingCancelled = false;
          this.isPollingPaused = false;
          this.createPollingEffect(); // <--- recreate the effect every time
        }),        
          mergeMap(() =>
            this.appDataService.getData()
              .pipe(                        
                switchMap(data => {              
                  return [appActions.getDataSuccess(data)
                  ];
                  }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ));

      public pausePolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.pausePolling),
        tap(_ => this.isPollingPaused = true),
        tap(_ => console.log('app effect pause polling')),
      ), { dispatch: false });

      public cancelPolling$ = createEffect(() => this.actions$.pipe(
        ofType(appActions.cancelPolling),
        tap(_ => {
          this.isPollingCancelled = true;
          this.isPollingPaused = true;
        }),
        tap(_ => console.log('app effect cancel polling')),
      ), { dispatch: false });

      public continuePolling$: any;

      private createPollingEffect(): void {
        console.log('creating continuePolling$');
        this.continuePolling$ = createEffect(() => this.actions$.pipe(
          ofType(appActions.getDataSuccess, appActions.getDataFail),
          tap(data => console.log('app effect continue polling')),
          delay(3000),
          takeWhile(() => !this.isPollingCancelled),
          mergeMap(() =>
            this.appDataService.getData(false)
              .pipe(
                tap(data => console.log('app effect continue polling - inner loop')),

                switchMap(data => {
                  return [appActions.getDataSuccess(data)
                  ];
                }),
                catchError(err => of(appActions.getDataFail(err)))
              ))
        ), { resubscribeOnError: true });
      } 
    }

因此,在startPolling中,我叫this.createPollingEffect()来创建持续轮询效果。

但是,当我尝试这样做时,轮询永远不会开始。

4 个答案:

答案 0 :(得分:0)

改用它:

public startPolling$ = createEffect(() => this.actions$.pipe(
  ofType(appActions.startPolling),    
  tap(_ => console.log('app effect started polling')),  
  tap(() => this.isPollingActive = true),        
  switchMap(() =>
    this.appDataSurvice.getData()
      .pipe(                        
        exhaustMap(data => {              
          return [appActions.getDataSuccess(data)];
        }),
        catchError(err => of(appActions.getDataFail(err)))
      ))
));

答案 1 :(得分:0)

您解决问题的方式值得称赞。重新启动轮询时,我遇到了完全相同的问题,这篇文章对我有所帮助。

我现在面临的一个问题是,如果在不到3秒的时间内重新启动轮询(指定了计时器),则会有多次对该服务的调用。换句话说,轮询仅在间隔过去之后才完全暂停/停止。因此,如果您尝试在计时器过去之前再次启动它,则有多个线程在运行。刚刚在服务调用@ https://angular-ngrx-polling3-j7b8st.stackblitz.io

中添加了时间戳

每次民意测验都将进行两次服务呼叫。

答案 2 :(得分:0)

我把这个作为我的问题/讨论的一部分,但是我认为可以作为一种解决方案,以使其更加可见...

我想出了一个对我有用的解决方案。

我有以下

public startPolling$ = createEffect(() => this.actions$.pipe(
        ofType(dataActions.startPollingGetData),
        tap(_ => this.logger.info('effect start polling')),
        tap(() => this.isPollingActive = true),
        switchMap(_ => this.syncData())
      ), { dispatch: false });

    public continuePolling$ = createEffect(() => this.actions$.pipe(
        ofType(dataPlannerActions.DataSuccess,
          dataActions.DataFail),
        tap(_ => this.logger.debug('data effect continue polling')),
        tap(_ => this.isInDelay = true),
        delay(8000),
        tap(_ => this.isInDelay = false),
        switchMap(_ => this.syncData())
      ), { dispatch: false });


    public stopPolling$ = createEffect(() => this.actions$.pipe(
        ofType(dataActions.stopPollingData),
        tap(_ => this.isPollingActive = false),
        tap(_ => this.logger.info('data effect stop polling')),
        map(_ => dataActions.stopPollingDataSuccess())
      ), { dispatch: false });


    private syncData(): Observable<Action> {
        const result$: Observable<Action> = Observable.create(async subscriber => {
          try {
            // If polling "switched off", we just need to return anything (not actually used)
            // Id isInDelay, we may be restating while we still have a pending delay.
            // In this case we will exit, and just wait for the delay to restart
            // (otherwise we can end up with more than one call to this)
            if (this.isInDelay || !this.isPollingActive) {
              subscriber.next("");
              return;
            }

我在这里使用了几个“标志”,我相信这样做会更“ rxy”。

实际上,see this post是如何摆脱isInDelay的方法(我只需要解决这个问题,并将其放入上面的生产代码中即可)

答案 3 :(得分:0)

基于@peterc和@IngoBürk的输入,我能够测试所有积极的情况。以下是我的代码的外观。

@Effect()
      getPageData$ = this.actions$.pipe(
        ofType(actions.StartLoading),
        tap(() => {
          this.appService.isPollingActive = true;
        }),
        mergeMap(() =>
          this.appService.getData().pipe(
            switchMap((response: GridDataResponse) => {
              return [new actions.DoneLoading(response.data)];
            }),
            retry(1),
            catchError(err => {
              return of(new actions.FailedLoading());
            })
          ))
      );

      @Effect()
      public stopPolling$ = this.actions$.pipe(
        ofType(actions.StopPolling),
        tap(_ => {
          this.appService.isPollingActive = false;
        }),
        mergeMap(() => {
          return [new actions.ResetLoading()];
        })
      );

      @Effect()
      public continuePolling$ = this.actions$.pipe(
        ofType(actions.DoneLoading,
          actions.FailedLoading),
        switchMap(_ =>
          timer(this.appService.pollingTimer).pipe(
            takeUntil(this.actions$.pipe(ofType(actions.StopPolling))),
            mergeMap(() =>
            this.appService.getData().pipe(
              takeWhile(() => this.appService.isPollingActive),
              switchMap((response: GridDataResponse) => {
                return [new actions.DoneLoading(response.data)];
              }),
              catchError(err => {
                return of(new actions.FailedLoading());
              })
            ))
          )
      )
      );