响应式开发 | RxJS 异步管道不工作

Reactive Development | RxJS async pipe not working

我正在尝试通过将我在服务中的旧方法转换为在 Observable 中完全不订阅来学习 Angular 中的反应式开发。到目前为止,我已经转换的最简单的方法是 getAll 方法。我现在想要的是,当我在卡片列表中选择一张卡片时,我将被重定向到另一个页面以查看它的详细信息。考虑以下我目前正在使用的代码。

服务

@Injectable({
  'providedIn': 'root'
})
export class CardService {

  private selectedCardIdSubject = new Subject<number>();
  selectedCardIdAction$ = this.selectedCardIdSubject.asObservable();

 /**
   * Retrieve single card based on card id.
   */
  card$ = this.selectedCardIdAction$
    .pipe(
      tap(id => console.log(`card$: ${this._cardsApi}/${id}`)),
      concatMap(id =>
        this._http.get<Card>(`${this._cardsApi}/${id}`)
          .pipe(
            map(card => ({
              ...card,
              imageUrl: `${this._cardImageApi}/${card.idName}.png`
            }))
          )
      ),
      catchError(this.handleError)
    );

  onSelectedCardId(id: number) {
    this.selectedCardIdSubject.next(id);
    console.log(`onSelectedCardId: ${id}`)
  }    
}

组件

@Component({})
export class CardDetailsComponent {
    constructor(private _cardService: CardService,
        private route: ActivatedRoute) { 

        this.selectedCardId = this.route.snapshot.params['id'];
        this._cardService.onSelectedCardId(this.selectedCardId);
    }

    card$ = this._cardService.card$;
}

HTML

<div class="container mb-5" *ngIf="card$ | async as card">
    <p>{{ card.name }}</>
</div>

在上面的代码中,当重定向时,它不会呈现我的简单 HTML 来显示所选卡片的名称。我正在使用 [routerLink] 在卡详细信息页面中重定向。

卡片列表组件

<div *ngIf="cards$ | async as cards">
    <div class="col-md-1 col-4 hover-effect" *ngFor='let card of cards'>
      <a class="d-block mb-1">
        <figure>
          <img class="img-fluid img-thumbnail" [src]='card.imageUrl' [title]='card.name' [alt]='card.name'
            [routerLink]="['/cards', card.id]" />
        </figure>
      </a>
    </div>
</div>

谁能告诉我这里发生了什么?

它不工作的原因是你的 $card 是在你的构造函数中已经发射发射之后订阅的,此时视图还没有呈现,这意味着 aysnc 管道还没有到位。

场景很像下面的代码顺序,因此你没有收到更新

// constructor
const a=new Subject()
a.next('some value')
// rendering
a.subscribe(console.log) // never triggered

有两种方法可以解决这个问题:

  • 1st 用 shareReplay(1) 缓存最新值,所以它总是在订阅时发出最后一个值

    selectedCardIdAction$ = this.selectedCardIdSubject.asObservable().pipe(shareReplay(1));
    

或使用BehaviorSubject

private selectedCardIdSubject = new BehaviorSubject<number | null>(null);
  • 第二次移动 .next() 到 ngAfterViewInit - 它在创建视图并订阅异步管道后触发

我个人会选择第一种方法,如果您的代码没有太多缺陷,结果会更一致

我认为还有第二个陷阱:

在您的组件代码中,您通过快照访问路由参数的位置:this.route.snapshot.params['id']

由于只调用一次构造函数,即使参数发生变化,我怀疑您的解决方案在更改所选卡时是否有效。

最佳做法是通过 RxJs 订阅路由参数。为了避免不必要的订阅,我们可以使用高阶可观察运算符 mergeMap 在每次路由参数更改时加载单个卡片条目

@Component({
  selector: "card",
  template: `
    CARD_DETAIL
    <div *ngIf="card$ | async; let card">Test {{ card.name }}</div>
  `
})
export class CardComponent {
  card$;

  constructor(private cardService: CardService, private route: ActivatedRoute) {
    this.card$ = this.route.params.pipe(
      map(getIdParameter),
      mergeMap(this.cardService.loadSingleCard)
    );
  }
}

const getParameter = param => params => params[param];
const getIdParameter = getParameter("id");

这也会清理您的服务并将其减少到一个 BehaviorSubject:

@Injectable()
export class CardService {
  card$ = new BehaviorSubject<Card>(null);

  constructor(private httpClient: HttpClient) {}

  loadSingleCard = (id: number) => {
    const url = `https://api.punkapi.com/v2/beers/${id}`;

    this.httpClient
      .get<Card>(url)
      .pipe(map(cards => cards[0]))
      .subscribe(card => this.card$.next(card));

    return this.card$;
  };
}

编辑:

以下是所谓的"Arrow function expression",它使用函数式方法,称为"Partial application" (afaik)。在我们的例子中,它是一个接受特定参数的函数和 returns 另一个接受整个参数对象的函数。当 returned 函数被调用时,我们终于能够从 params 对象中 return 指定的参数。

const getParameter = param => params => params[param];

等同于:

const getParameter = function(param) {
    return function(params) {
       params[param];
    };
}

const getIdParameter = getParameter("id");

// Could be called like that aswell:
const params = { id: 1, name: 'chris'};
const idParam = getParameter("id")(params); // hence the name partial application, i guess ;)

我同意它看起来确实很花哨,但现在我们可以将 getIdParameter 函数引用传递给 map(getParameterdId),这基本上是一个较短的语法:map(params => getParameterId(params)map(params => getParameters(params, 'id'))

My Playground

干杯克里斯