Observable 只更新 UI 一次
Observable updates UI only once
我对 Angular (10) 还很陌生,并尝试掌握 Observables 的概念。我有一个服务和一个组件,服务用参与者填充一个数组,组件应该显示它们
服务
export class MercureService {
private _participants = new BehaviorSubject<ParticipantDTO[]>([]);
private participantList: Array<ParticipantDTO> = [];
constructor() {
}
get participants$(): Observable<Array<ParticipantDTO>> {
return this._participants.asObservable();
}
admin_topic(topic: AdminTopic): void {
const url = new URL(environment.mercure);
url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = event => {
const json = JSON.parse(event.data);
console.log('NEW EVENT');
// ...
if (json.event === BackendEvents.NEW_PARTICIPANT) {
this.participantList.push({voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0'});
this._participants.next(this.participantList);
}
};
}
Component.ts
export class StartSessionComponent implements OnInit, OnDestroy {
// ...
unsubscribe$: Subject<void> = new Subject<void>();
participantList: ParticipantDTO[];
constructor(
// ...
public mercure: MercureService
) {}
ngOnInit(): void {
this.mercure.participants$
.pipe(takeUntil(this.unsubscribe$))
.subscribe((data) => {
this.participantList = data;
});
this.mercure.admin_topic({sessionUuid: '123', userUuid: '456'});
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
Component.html
...
<div *ngFor="let participant of this.mercure.participants$ | async">
<p>{{participant.voter}} - Vote ({{participant.vote}})</p>
</div>
...
所以我没有发送消息,它被 EventSource 接收,console
说
NEW EVENT
并且 UI 得到更新(添加一个新的 <p>WHATEVER NAME - VOTE XXX</p>
)。但是,当我从服务器发送第二条消息时,我得到
NEW EVENT
,但 UI 没有更新。我怀疑我对 Observable 做错了什么,有人能帮忙吗?
这是预期的行为,因为 this.participantList
指的是主题已存储的值(因为引用未更改),您可能希望展开数组以创建新数组每次您想更新其值时:
this._participants.next(....this.participantList);
问题是 EventSource 事件是在 Angular 之外发出的,因此无论在 eventSource.onmessage
内部发生什么,都不会正确更新 UI。这就是为什么你需要在 NgZone 的帮助下将 onmessage
中发生的任何事情包装成 运行 中的 Angular。
参见示例:
constructor(
private zone: NgZone
) { }
admin_topic(topic: AdminTopic): void {
const url = new URL(environment.mercure);
url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = event => {
this.zone.run(() => { // This is what you need
const json = JSON.parse(event.data);
console.log('NEW EVENT');
// ...
if (json.event === BackendEvents.NEW_PARTICIPANT) {
this.participantList.push({ voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0' });
this._participants.next(this.participantList);
}
})
};
}
另一种解决方案是使用事件源包装器,它可以为您做完全相同的事情,并且使您易于使用。它也会被包裹在Observable中,让你拥有丰富的体验。例如,参见这篇文章:Event Source wrapped with NgZone and Observable
我对 Angular (10) 还很陌生,并尝试掌握 Observables 的概念。我有一个服务和一个组件,服务用参与者填充一个数组,组件应该显示它们
服务
export class MercureService {
private _participants = new BehaviorSubject<ParticipantDTO[]>([]);
private participantList: Array<ParticipantDTO> = [];
constructor() {
}
get participants$(): Observable<Array<ParticipantDTO>> {
return this._participants.asObservable();
}
admin_topic(topic: AdminTopic): void {
const url = new URL(environment.mercure);
url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = event => {
const json = JSON.parse(event.data);
console.log('NEW EVENT');
// ...
if (json.event === BackendEvents.NEW_PARTICIPANT) {
this.participantList.push({voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0'});
this._participants.next(this.participantList);
}
};
}
Component.ts
export class StartSessionComponent implements OnInit, OnDestroy {
// ...
unsubscribe$: Subject<void> = new Subject<void>();
participantList: ParticipantDTO[];
constructor(
// ...
public mercure: MercureService
) {}
ngOnInit(): void {
this.mercure.participants$
.pipe(takeUntil(this.unsubscribe$))
.subscribe((data) => {
this.participantList = data;
});
this.mercure.admin_topic({sessionUuid: '123', userUuid: '456'});
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
Component.html
...
<div *ngFor="let participant of this.mercure.participants$ | async">
<p>{{participant.voter}} - Vote ({{participant.vote}})</p>
</div>
...
所以我没有发送消息,它被 EventSource 接收,console
说
NEW EVENT
并且 UI 得到更新(添加一个新的 <p>WHATEVER NAME - VOTE XXX</p>
)。但是,当我从服务器发送第二条消息时,我得到
NEW EVENT
,但 UI 没有更新。我怀疑我对 Observable 做错了什么,有人能帮忙吗?
这是预期的行为,因为 this.participantList
指的是主题已存储的值(因为引用未更改),您可能希望展开数组以创建新数组每次您想更新其值时:
this._participants.next(....this.participantList);
问题是 EventSource 事件是在 Angular 之外发出的,因此无论在 eventSource.onmessage
内部发生什么,都不会正确更新 UI。这就是为什么你需要在 NgZone 的帮助下将 onmessage
中发生的任何事情包装成 运行 中的 Angular。
参见示例:
constructor(
private zone: NgZone
) { }
admin_topic(topic: AdminTopic): void {
const url = new URL(environment.mercure);
url.searchParams.append('topic', `${topic.sessionUuid}_${topic.userUuid}`);
const eventSource = new EventSource(url.toString());
eventSource.onmessage = event => {
this.zone.run(() => { // This is what you need
const json = JSON.parse(event.data);
console.log('NEW EVENT');
// ...
if (json.event === BackendEvents.NEW_PARTICIPANT) {
this.participantList.push({ voter: json.data.voter, voterUuid: json.data.voterUuid, vote: '0' });
this._participants.next(this.participantList);
}
})
};
}
另一种解决方案是使用事件源包装器,它可以为您做完全相同的事情,并且使您易于使用。它也会被包裹在Observable中,让你拥有丰富的体验。例如,参见这篇文章:Event Source wrapped with NgZone and Observable