Angular + NgRx;多次 API 调用的问题

Angular + NgRx; Issues with Multiple API calls

我正在开发一个 Angular 应用程序,该应用程序通过 Nx 生成并使用 NgRx 进行状态管理。 随着应用程序的规模,我面临着多次 API 调用的问题,我不知道为什么。

这是我的结构:

app.module.ts

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    BrowserAnimationsModule,
    HttpClientModule,

    HammerModule,

    // Firebase
    AppFirebaseModule,

    // Translation
    AppTranslateModule,

    // Main Root Store
    StoreModule.forRoot({
      [fromAuth.AUTH_FEATURE_KEY]: authReducers.reducer,
      [fromProfile.profileFeatureKey]: profileReducers.reducer,
    }),
    EffectsModule.forRoot([AuthEffects, ProfileEffects]),
    !environment.production ? StoreDevtoolsModule.instrument() : [],
    StoreRouterConnectingModule.forRoot(),

    // Service Worker
    ServiceWorkerModule.register('ngsw-worker.js', {
      enabled: environment.production,
      // Register the ServiceWorker as soon as the app is stable
      // or after 30 seconds (whichever comes first).
      registrationStrategy: 'registerWhenStable:30000',
    }),

    // App Routing Module
    AppRoutingModule,

    SsCommonModule,
    NgxMaskModule.forRoot(),

    RepositoryModule,
  ],
  providers: [
    EnvironmentService,
    AppRoutesService,
    CookieService,
    AppRouteMaps,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ApiInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

我把所有API调用都放在了Repository模块中,如下:

repository.module.ts

@NgModule({
  imports:[
  ],
  providers:[
    GirlHelper,
    GirlRepository,
    ProfileHelper,
    ProfileRepository,
    PhotoRepository,
    ImageRepository,
    ChatRepository,
    UserRepository,
    SettingsRepository,
    NewsRepository,
    EventRepository,
    ServiceRepository
  ],
  exports: [ ]
})
export class RepositoryModule {}

每个存储库都按提供的方式导出,因此我可以在任何地方使用它们。每个 service/repository 的格式如下(就像一个简单的数据服务):

@Injectable()
export class ProfileRepository {
    private api = this.env.get('apiUrl') + 'profile/';

    constructor(
        private env: EnvironmentService,
        private http: HttpClient,
        private helper: ProfileHelper) { }

    getAll(filters: ProfileListFilters):  Observable<Profile[]> {
        return this.http.get<Profile[]>(this.api, {
            params: { ...filters }
        }).pipe(
            map((data: any) => data?.result),
            catchError((err) => this.onError(err))
        );
    }
}

我为我的应用程序的不同功能创建了功能模块,并将它们放在各自的文件夹中,就像普通功能模块一样。

例如client/client.module.ts

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(routes),
    SsCommonModule,
    ClientStoreModule
  ],
  declarations: [
    ClientComponent
  ],
})
export class ClientModule {}

我也在应用程序路由中延迟加载这些功能模块。module.ts:

const routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  {
    path: '',
    canActivate: [CanActivateSync],
    children: [
      {
        path: 'home',
        loadChildren: () => import('@myapp/home').then((m) => m.HomeModule),
      },
      {
        path: 'auth',
        children: authRoutes,
      },
      {
        path: 'client',
        loadChildren: () =>
          import('@myapp/client').then((module) => module.ClientModule),
      },
      {
        path: 'admin',
        canActivate: [CanActivateAdmin],
        loadChildren: () =>
          import('@myapp/admin').then((module) => module.AdminModule),
      }
      ....
    ],
  },
  {
    path: '**',
    redirectTo: '/',
  },
  {
    path: 'events',
    loadChildren: () =>
      import('@myapp/events').then((module) => module.EventsModule),
  },
  {
    path: 'news',
    loadChildren: () =>
      import('@myapp/news').then((module) => module.NewsModule),
  },
];

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forRoot(routes, { initialNavigation: 'enabled' }),
    AuthModule,
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

另一方面,我的 NgRx 存储在这些功能模块中,在 +state 文件夹中并按它们各自的文件划分(例如动作、模型等,通常的 ngrx 东西) 我还有一个单独的模块用于每个功能状态:

客户端/+state/client-store.module.ts

@NgModule({
  imports: [
    CommonModule,
    StoreModule.forFeature(fromClient.clientFeatureKey, reducer),
    EffectsModule.forFeature([ClientEffects])
  ],
  providers: []
})
export class ClientStoreModule {}

然后将这些商店模块导入到它们各自的功能模块中,如下所示:

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(routes),
    SsCommonModule,
    ClientStoreModule
  ],
  declarations: [
    ClientComponent
  ],
})
export class ClientModule {}

相应的存储库被注入到功能模块的效果上以触发 API 调用:

@Injectable()
export class ClientEffects {

  loadChatGroups$ = createEffect(() => this.actions$.pipe(
    ofType(ChatActions.loadChatGroups),
    mergeMap((action) => this.profileRepo.getChatGroups(action?.payload?.uid, action?.payload?.filters)),
    mergeMap(chatGroups => of(ChatActions.loadChatGroupsSuccess({ payload: chatGroups }))),
    catchError(error => of(ChatActions.loadChatGroupsFailure(error)))
  ));

  loadChatGroupMessages$ = createEffect(() => this.actions$.pipe(
    ofType(ChatActions.loadChatMessages),
    mergeMap((action) => this.profileRepo.getChatGroupMessages(action?.payload?.uid, action?.payload?.chatGroupId, action?.payload?.filters)),
    mergeMap(messages => of(ChatActions.loadChatMessagesSuccess({ payload: messages }))),
    catchError(error => of(ChatActions.loadChatMessagesFailure(error)))
  ));
  
  sendMessage$ = createEffect(() => this.actions$.pipe(
    ofType(ChatActions.sendMessage),
    mergeMap((action) => this.profileRepo.sendChatGroupMessage(
      action?.payload?.uid,
      action?.payload?.chatGroupId!,
      action?.payload?.content,
      action?.payload?.contentType
    ).pipe(
      mergeMap(response => of(ChatActions.sendMessageSuccess({ payload: new ProfileChatMessage(action.payload) }))),
    )),    
    catchError(error => of(ChatActions.sendMessageFailure(error)))
  ));
  
}

所以主要问题是我有多个 API 调用,我不知道为什么,一个例子只是在单击按钮时调用 API 调用(sendMessage 函数是在HTML中的(点击)事件中绑定:

sendMessage(chatGroupId: number, ev: Event): void {
    ev.preventDefault();
    const message = this.messageFC.value;

    if(message !== '') {
      this.messageFC.setValue('');
      
      this.store.dispatch(ChatActions.sendMessage({ payload: {
        chatGroupId: chatGroupId,
        content: message,
        contentType: ProfileChatMessageType.Message,
        uid: this.client?.id!
      }}));
    }
}

现在,即使我只是直接调用 http 客户端调用,也会有多个 API 调用:

sendMessage(chatGroupId: number, ev: Event): void {
    ev.preventDefault();
    const message = this.messageFC.value;

    if(message !== '') {
      this.messageFC.setValue('');
      console.log('====MAIN START===');
      
      this.http.post<boolean>(this.api + '/chats/send-message', {
      chat_id: chatGroupId,
      profile_id: this.client?.id,
      content: message,
      content_type: ProfileChatMessageType.Message,
    }).subscribe();
    
}

我开发angular应用这么久了,还没遇到过这个问题。这是第一次尝试NgRx,因为他们说它对大型应用程序有益,现在我的应用程序足够大但恰恰相反。

有人遇到过这个问题吗?这应该是一个简单的 API 调用,现在我不确定为什么它会调用多个 API 调用。 是 NgRx 吗?是关于我如何构建商店模块吗?我应该删除 ngrx 吗?

非常感谢任何帮助,我现在被困住了,快要放弃 Ngrx 了。请救救我。

谢谢。

一些事情...

  • 确保你的行为types/descriptions是独一无二的。已经说不出多少次了。

  • 您将 catchError 放置在 Effect 的错误位置,并且正在调用不必要的 mergeMap

    sendMessage$ = createEffect(() => this.actions$.pipe(
        ofType(ChatActions.sendMessage),
        mergeMap((action) => this.profileRepo.sendChatGroupMessage(
          action?.payload?.uid,
          action?.payload?.chatGroupId!,
          action?.payload?.content,
          action?.payload?.contentType
        ).pipe(
          mergeMap(response => of(ChatActions.sendMessageSuccess({ payload: new ProfileChatMessage(action.payload) }))),
        )),    
        catchError(error => of(ChatActions.sendMessageFailure(error)))
      ));
    

试试这个:

sendMessage$ = createEffect( () => this.actions$.pipe(
  ofType( ChatActions.sendMessage ),
  mergeMap( ( { payload } ) => this.profileRepo.sendChatGroupMessage(
    payload?.uid,
    payload?.chatGroupId!,
    payload?.content,
    payload?.contentType
  ).pipe(
    **map**( response => of( ChatActions.sendMessageSuccess( { payload: new ProfileChatMessage( action.payload ) } ) ) ),
    catchError( error => of( ChatActions.sendMessageFailure( error ) ) )
  ) )
) );

并更改此:

>   loadChatGroups$ = createEffect(() => this.actions$.pipe(
>     ofType(ChatActions.loadChatGroups),
>     mergeMap((action) => this.profileRepo.getChatGroups(action?.payload?.uid,
> action?.payload?.filters)),
>     mergeMap(chatGroups => of(ChatActions.loadChatGroupsSuccess({ payload: chatGroups }))),
>     catchError(error => of(ChatActions.loadChatGroupsFailure(error)))   ));

  loadChatGroups$ = createEffect(() => this.actions$.pipe(
    ofType(ChatActions.loadChatGroups),
    mergeMap((action) => this.profileRepo.getChatGroups(action?.payload?.uid, action?.payload?.filters).pipe(
      **map**(chatGroups => of(ChatActions.loadChatGroupsSuccess({ payload: chatGroups }))),
      catchError(error => of(ChatActions.loadChatGroupsFailure(error)))
    ))    
  ));

我发现导致多个 http requests.And 的问题不是 ng-rx,也不是我的应用程序发送的任何事件(感谢上帝,它不是 ng-rx,因为我喜欢用它),最后,它是 firebase。

"@angular/fire": "^7.0.4",

所以,简而言之,我有 firebase Auth 来保留我当前登录的用户,我有一个 HTTP 拦截器来获取 Bearer 令牌以传递我的 http 调用,我的拦截器如下所示:

export default class ApiInterceptor implements HttpInterceptor {

  constructor(@Optional() private auth: Auth) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {

    if (!request.url.includes("api")) return next.handle(request);

    return next.handle(request).pipe(
        mergeMap(req => authState(this.auth).pipe(
          shareReplay(),
          mergeMap((user: any) => user ? from(user!.getIdToken()) : of(null)),
          mergeMap(token => {
              if(!token) return next.handle(request);
              const headers = new HttpHeaders({
                  'Authorization': 'Bearer ' + token
              });
              const newRequest = request.clone({ headers });

              return next.handle(newRequest);
          })
        ))
    );
  }
}

问题出在这一行:

authState(this.auth).pipe(...)

这导致我的http调用被多次触发,我不知道为什么(如果有人知道请回复)。

所以我所做的就是获取当前用户并调用 getIdToken() 并将其转换为 Observable。

 const user = this.auth.currentUser;

    return from(user.getIdToken()).pipe(
      mergeMap(token => {
          if(!token) return next.handle(request);
          const headers = new HttpHeaders({
              'Authorization': 'Bearer ' + token
          });
          const newRequest = request.clone({ headers });

          return next.handle(newRequest);
      })
    );

现在我的 HTTP 调用按预期运行。万岁!