首页 > 解决方案 > 使用变量突变器改变 RxJS 可观察对象 - Angular

问题描述

我做了一个从远程源获取一些用户数据的服务。该服务是一种获取多个用户的方法,以及一种获取特定用户的方法。从这两种方法返回的 observables 通过 map() 获得 .pipe(ed) 以便能够在用户对象被消耗之前对其进行变异。

我想要的是只为多用户流和单用户流定义一次修改器,但我目前的方法遇到了范围问题。

请注意,我称用户为“英雄”。这是一个设计方面。以下是我的 HeroService 类中的相应方法:

export class HeroService {
  // [...]

  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.addSkillsToHero)),
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.splitName))
    );
  }

  getHero(id): Observable<Hero> {
    return this.http.get<Hero>(this.heroesUrl + "/" + id).pipe(
      map((hero: Hero) => this.addSkillsToHero(hero)),
      map((hero: Hero) => this.splitName(hero))
    );
  }

  private mutateHeroes(heroes: Hero[], mutator) {
    heroes.forEach((hero: Hero) => {
      hero = mutator(hero);
    });
    return heroes;
  }

  private splitName(hero: Hero) {
    let heroNames: string[] = hero.name.split(" ");

    hero.firstname = heroNames.splice(0, 1).join(" ");
    hero.lastname = heroNames.splice(heroNames.length - 1, 1).join(" ");
    hero.middlename = heroNames.join(" ");

    return hero;
  }

  private addSkillsToHero(hero: Hero) {
    hero.skills = this.getSkills(Math.ceil(Math.random() * 10));
    return hero;
  }

  private getSkills(count: number): string[] {
    let skills: string[] = [];
    let i;

    for (i = 0; i < count; i++) {
      skills.push(this.getRandomSkill());
    }

    return skills;
  }

  private getRandomSkill(): string {
    return SKILL_TAGS[Math.floor(Math.random() * SKILL_TAGS.length)];
  }
  // [...]
}

Observable(s) 的 catchError() 返回:Cannot read property 'getSkills' of undefined 我怀疑 mutator 没有在类范围内被调用,因此无法找到。

我将如何在 JS 中做这样的事情?

整个项目可以在以下位置进行检查:

标签: angularrxjsrxjs-observablesrxjs-pipeable-operators

解决方案


用于救援的箭头函数 (Lamdas)

如前所述,您的问题归结为如何this确定函数的上下文 ( )。这是在运行时完成的。

this要扩展此处给出的另一个答案,您可以定义一个已绑定其上下文 ( ) 的函数。诀窍是尽早调用该函数并使用该函数的部分应用版本。一种方法是使用bind.

const functionName = (function (args){
  /* Some code */
  return /*something*/
}).bind(this);

这有点难看,所以 JavaScript 有箭头函数可以做同样的事情。

const functionName = (args) => {
  /* Some code */
  return /*something*/
}

这样漂亮多了。更好的是单行单表达式 lamdas 有一个隐式返回:

const functionName = (args) => {
  return /*something*/;
}
// Is the same as
const functionName = (args) => /*something*/;

普通函数在调用时会给出一个上下文。箭头函数总是将它们定义的范围绑定为它们的上下文。如果您不使用 Javascript 的原型继承,通常没有太多理由需要从定义它的范围更改函数的上下文。

我发现如果你愿意稍微了解一下这些知识,你真的可以整理你的代码。

下面是我将如何编写完全使用箭头函数的 HeroService 类。如果这是最好的方法,这是有争议的。当然,这种方法允许更多的声明式/函数式风格。在实践中,两者的混合让你两全其美。

export class HeroService {
  // [...]

  public getHeroes = (): Observable<Hero[]> =>
    this.http.get<Hero[]>(this.heroesUrl).pipe(
      map(this.mutateHeroes(this.addSkillsToHero)),
      map(this.mutateHeroes(this.splitName))
    );

  public getHero = (id): Observable<Hero> => 
    this.http.get<Hero>(this.heroesUrl + "/" + id).pipe(
      map(this.addSkillsToHero),
      map(this.splitName)
    );

  private mutateHeroes = transformer => 
    (heroes: Hero[]) => heroes.map(transformer);

  private splitName = (hero: Hero) => {
    let heroNames: string[] = hero.name.split(" "); 
    return ({
      ...hero,
      firstName: heroNames.splice(0, 1).join(" "),
      lastname: heroNames.splice(heroNames.length - 1, 1).join(" "),
      middlename: heroNames.join(" ")
    });
  }

  private addSkillsToHero = (hero: Hero) => ({
    ...hero, skills: this.getSkills(Math.ceil(Math.random() * 10))
  });

  private getSkills = (count: number): string[] => 
    Array.from({length: count}).map(this.getRandomSkill);

  private getRandomSkill = (): string => 
    SKILL_TAGS[Math.floor(Math.random() * SKILL_TAGS.length)];
  
  // [...]
}

额外:组合函数

正确实现的映射函数的一个很酷的属性(无论您是映射数组还是流)是:

map(x => f(g(x))) 
  === 
map(g).map(f)

这告诉我们,如果我们 compose addSkillsToHerosplitName那么你可以用一个map而不是两个来做到这一点。RxJS 流实际上是转换器,因此它们(在某种程度上)为您执行此操作,但数组映射不会,因此您也可以通过组合获得一些边际性能(您只遍历数组一次!)。

通常你会给你一个像 Ramda/Redux/Lodash/etc 这样的库,你可以写类似的东西

private addSkillsAndSplitName = 
  compose(this.addSkillsToHero, this.splitName);

但是使用 vanilla JavaScript 组合两个函数也很容易。

private addSkillsAndSplitName = 
  hero => this.addSkillsToHero(this.splitName(hero))

然后你可以像这样使用这个新功能:

export class HeroService {
  // [...]

  public getHeroes = (): Observable<Hero[]> =>
    this.http.get<Hero[]>(this.heroesUrl).pipe(
      map(this.mutateHeroes(this.addSkillsAndSplitName))
    );

  public getHero = (id): Observable<Hero> => 
    this.http.get<Hero>(this.heroesUrl + "/" + id).pipe(
      map(this.addSkillsAndSplitName)
    );

而且您还没有对可测试性、灵活性或关注点分离产生影响,因为功能仍然存在于单独的函数中。


推荐阅读