以可观察的方式返回嵌套的forkjoins [英] Return nested forkjoins as observable

查看:72
本文介绍了以可观察的方式返回嵌套的forkjoins的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试返回一堆嵌套的forkjoins和我的解析器中的常规订阅.为此,我尝试使用地图,但我想我还没有完全掌握map/switchMaps/mergeMaps的概念. 我知道代码还没有返回UserResult,这是因为我还不知道如何将QuestionAnswers添加到UserResult中,但这对我当前的问题应该没有太大的区别.

我的目标是重写它,以便它返回可观察的结果.

 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);
                    }
                  });
              });
          });
        });
      });
    });
}

 

我试图像这样重写它,但是它根本不起作用.我遇到一些未定义的错误,但它们都指向管道的起点,没有具体说明.

     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);
                }
              })
          )
      )
    );
 

编辑

引起我注意的是,点击result2后res [0]引起

无法读取未定义的属性"0"

我认为这与我对水龙头的使用不当有关,因为它在我尝试更改的订阅中运行良好.

EDIT2

我将代码分成了类似Kurt推荐的较小函数,但是我不确定如何将其与我用于类别的forEach一起使用.我也不知道应该在哪里创建最终的对象,该对象将以可观察的方式返回

 
 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;

              )
          )
      )
    );

 

解决方案

所以...您到那里了很多RxJS.

首先,您不需要在RxJS运算符内进行订阅,而是将可观察对象链接在一起.

一些定义

switchMapconcatMap用于将一个可观察到的结果传递给另一个.

map用于将值从一种结构转换为另一种结构(类似于同名数组函数的概念).

forkJoin组合多个可观察对象,并在它们全部完成后返回一个结果.

您的代码

在您甚至开始尝试整理代码之前,我建议您考虑考虑将每个步骤拆分为自己的功能.希望这将帮助您查看数据流并考虑依赖项在哪里.

我可以将您的原始示例转换为RxJS,但是在考虑每个步骤实际上试图实现的目标时却迷失了一部分.

我确定的是,您最终会得到一个类似这样的模式(出于这个演示的目的,我正在订阅-您将返回可观察的结果):

 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(' ');
}
 

每个可观察的步骤都已移入其自己的功能-这可以帮助您考虑需要输入什么数据和要输出什么数据-您正在有效地在每个步骤之间创建合同.

switchMap只是采用一个可观察的位置并设置另一个.最后的map将前面的可观察值的输出转换为另一个值.

我在这里使用了字符串,希望可以简化跟踪数据流的过程.我建议从尝试理解我的简单示例开始,然后使用这些原理重新构建您的功能.

演示: https://stackblitz.com/edit/angular-eedbqg

我的版本在以下方面与您的版本大致保持一致:

初始值

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

forkJoinOne

所有派生联接都应传入数组或对象.我更喜欢传入对象的相对较新的方式,它指明了发射值的结构. (forkJoin({ a: myObs })返回{ 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

您将需要将此循环转换为可观察对象数组,并将其传递给forkJoin.

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

mapFour

您将需要整理地图.在这里而不是forEach,而更喜欢filtermap(数组函数).

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);
      }
    })

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;

              )
          )
      )
    );

解决方案

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);
      }
    })

这篇关于以可观察的方式返回嵌套的forkjoins的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆