响应式开发 | 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'))
干杯克里斯
我正在尝试通过将我在服务中的旧方法转换为在 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'))
干杯克里斯