Return nested forkjoins as observable

北城以北 提交于 2020-04-18 12:31:24

问题


I'm trying to return a bunch of nested forkjoins and normal subscribes in my resolver. For this I tried using maps, but I think I don't fully grasp the concept of maps/switchMaps/mergeMaps yet. I know the code doesnt return a UserResult yet, this is because I have no idea yet how I will add questionAnswers to the UserResult, but this shouldn't be that much of a difference for my current problem.

My goals is to rewrite this, so that it returns an observable.

resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<UserResult> {
    const questionAnswers = Array<QuestionAnswer>();

    this.rs.getResult(this.auth.token, route.params['id']).subscribe(res => {
      forkJoin(
        this.quizs.getCategoriesQuiz(this.auth.token, res.quizId),
        this.accs.getAccount(res.userId)
      ).subscribe(results => {
        forkJoin(
          this.accs.getUserDetails(results[1].access_token),
          this.as.getAnswers(this.auth.token)
        ).subscribe(results2 => {
          results[0].forEach(cat => {
            this.cs
              .getQuestionsCategory(this.auth.token, cat.id)
              .subscribe(questions => {
                results2[1]
                  .filter(ans => ans.userId === results[1].uid)
                  .forEach(a => {
                    const question = questions.find(q => q.id === a.questionId);
                    if (!isNullOrUndefined(question)) {
                      const category = results[0].find(
                        c => c.id === a.categoryId
                      );
                      const qa = new QuestionAnswer(question, a);
                      qa.category = category.name;
                      questionAnswers.push(qa);
                    }
                  });
              });
          });
        });
      });
    });
}

I tried rewriting it like this, but it is not working at all. I'm getting some undefined errors but they all point towards the start of the pipe, nothing specific.

    const questionAnswers = Array<QuestionAnswer>();
    let res;
    let res2;

    return this.rs.getResult(this.auth.token, route.params['id']).pipe(
      map((res: Result) =>
        forkJoin(
          this.quizs.getCategoriesQuiz(this.auth.token, res.quizId),
          this.accs.getAccount(res.userId)
        )
      ),
      tap(results => (res = results)),
      map(results =>
        forkJoin(
          this.accs.getUserDetails(results[1].access_token),
          this.as.getAnswers(this.auth.token)
        )
      ),
      tap(results2 => (res2 = results2)),
      map(
        res[0]
          .forEach(cat => {
            this.cs.getQuestionsCategory(this.auth.token, cat.id);
          })
          .map(questions =>
            res2[1]
              .filter(ans => ans.userId === res[1].uid)
              .forEach(a => {
                const question = questions.find(q => q.id === a.questionId);
                if (!isNullOrUndefined(question)) {
                  const category = res[0].find(c => c.id === a.categoryId);
                  const qa = new QuestionAnswer(question, a);
                  qa.category = category.name;
                  questionAnswers.push(qa);
                }
              })
          )
      )
    );

EDIT

It just came to my attention that res[0] after the tap of results2 is causing a

Cannot read property '0' of undefined

I assume this has something to do with my bad use of taps, since it is working fine in the subscribes that I'm trying to change.

EDIT2

I split the code in smaller functions like Kurt recommended, however I'm not so sure how to use this with the forEach I'm using for categories. I also have no clue where I'm supposed to create my final object which I'll be returning as an observable


 getResultByRouteParamId(route: ActivatedRouteSnapshot): Observable<Result> {
    return this.rs.getResult(this.auth.token, route.params['id']);
  }

  forkJoinQuizCategoriesAndAccount(
    result: Result
  ): Observable<[Category[], Account]> {
    return forkJoin(
      this.quizs.getCategoriesQuiz(this.auth.token, result.quizId),
      this.accs.getAccount(result.userId)
    );
  }

  forkJoinUserDetailsAndAnswers(results: [Category[], Account]) {
    return forkJoin(
      this.accs.getUserDetails(results[1].access_token),
      this.as.getAnswers(this.auth.token)
    );
  }

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<UserResult> {

    const questionAnswers = Array<QuestionAnswer>();
    let result: Result;
    let res: [Category[], Account];
    let res2: [User, Answer[]];

    return this.getResultByRouteParamId(route).pipe(
      tap(resu => result = resu),
      switchMap((result: Result) => this.forkJoinQuizCategoriesAndAccount(result)),
      tap(results => (res = results)),
      switchMap(results => this.forkJoinUserDetailsAndAnswers(results)),
      tap(results2 => (res2 = results2)),
      switchMap(
          // Stuck here!
        res[0]
          .forEach(cat => {
            this.cs.getQuestionsCategory(this.auth.token, cat.id);
          })
          .map(questions =>
            res2[1]
              .filter(ans => ans.userId === res[1].uid)
              .forEach(a => {
                const question = questions.find(
                  (q: Question) => q.id === a.questionId
                );
                if (!isNullOrUndefined(question)) {
                  const category = res[0].find(
                    (c: Category) => c.id === a.categoryId
                  );
                  const qa = new QuestionAnswer(question, a);
                  qa.category = category.name;
                  questionAnswers.push(qa);
                }
              }
              // let ur = new UserResult(res2[1], result)
              // ur.questionAnswers = questionAnswers;
              // return ur;

              )
          )
      )
    );


回答1:


So... that's quite a chunk of RxJS you got there.

First things first - you don't subscribe inside RxJS operators - you chain observables together.

Some definitions

switchMap and concatMap are used for piping the result of one observable to another.

map is for transforming a value from one structure to another (similar to the concept of the array function of the same name).

forkJoin combines multiple observables and returns one result when they all complete.

Your code

Before you even start to try to straighten out your code, I would recommend thinking about splitting each step into its own function. This will hopefully help you to see the flow of data and think about where your dependencies are.

I had a go at converting your original example to RxJS, but got a bit lost when thinking about what each step was actually trying to achieve.

What I did ascertain is that you will end up with a pattern a bit like this (I am subscribing for the purposes of this demo - you would return the observable):

result: string;

ngOnInit() {
  this.initialValue().pipe(
    switchMap(result => this.forkJoinOne(result)),
    switchMap(result => this.forkJoinTwo(result)),
    switchMap(result => this.forkJoinThree(result)),
    map(result => this.mapFour(result))
  ).subscribe(result => {
    this.result = result;
  });
}

private initialValue(): Observable<string> {
  return of('zero');
}

private forkJoinOne(result: string): Observable<string[]> {
  return forkJoin([
    of(`${result} one`),
    of('four')
  ]);
}

private forkJoinTwo(results: string[]): Observable<string[]> {
  return forkJoin([
    of(`${results[0]} two`),
    of(`${results[1]} five`)
  ]);
}

private forkJoinThree(results: string[]): Observable<string[]> {
  return forkJoin([
    of(`${results[0]} three`),
    of(`${results[1]} six`)
  ]);
}

private mapFour(results: string[]): string {
  return results.join(' ');
}

Each observable step has been moved into it its own function - this helps you to think about what data needs to come in and what is coming out - you are effectively creating a contract between each step.

The switchMap is just taking one observable and setting up another. The final map is taking the output of the preceding observable and transforming it into a different value.

I have used strings here to hopefully make it easy to follow the flow of the data. I would recommend starting by trying to understand my simple example, and then re-building your function using the principles.

DEMO: https://stackblitz.com/edit/angular-eedbqg

My version roughly aligns with yours in the following ways:

initialValue

this.rs.getResult(this.auth.token, route.params['id'])

forkJoinOne

All fork joins should pass in an array or an object. I prefer the relatively new way of passing in objects, which dicates the structure of the emitted value. (forkJoin({ a: myObs }) returns { a: value }).

forkJoin(
  this.quizs.getCategoriesQuiz(this.auth.token, res.quizId),
  this.accs.getAccount(res.userId)
)

forkJoinTwo

forkJoin(
  this.accs.getUserDetails(results[1].access_token),
  this.as.getAnswers(this.auth.token)
)

forkJoinThree

You will need to convert this loop to an array of observables, and pass that in to a forkJoin.

results[0].forEach(cat => {
  this.cs.getQuestionsCategory(this.auth.token, cat.id)

mapFour

You will need to sort out your map. Instead of forEach here, prefer filter and map (the array function).

map(questions =>
  res2[1]
    .filter(ans => ans.userId === res[1].uid)
    .forEach(a => {
      const question = questions.find(q => q.id === a.questionId);
      if (!isNullOrUndefined(question)) {
        const category = res[0].find(c => c.id === a.categoryId);
        const qa = new QuestionAnswer(question, a);
        qa.category = category.name;
        questionAnswers.push(qa);
      }
    })


来源:https://stackoverflow.com/questions/60546308/return-nested-forkjoins-as-observable

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!