我不确定哪种首选的Angular方法可以解决模板基于两个不同来源显示信息的问题。
例如,我有一个存储在数据库中的联系人列表(此处显示为组件中的硬编码数组)。每个联系人都有来自网络事件的状态信息,并且否存储在数据库中。该状态信息将收集并存储在PresenceService
中。
@Injectable()
export class PresenceService {
private readonly onlineUsers: { [s: string]: boolean; }; // {uid: online}
constructor() {
// demo data, normally comes from network events
this.onlineUsers = { 1: true, 2: false };
}
isOnline(uid: string) {
return this.onlineUsers[uid];
}
}
我想显示包含状态信息的联系人列表:
@Component({
selector: 'my-app',
template: `
<h3> Your contacts </h3>
<p *ngFor="let c of contacts">
{{c.name}} is {{presence.isOnline(c.uid) ? 'online' : 'offline'}}
</p>
`
})
export class AppComponent {
contacts = [{
uid: 1,
name: 'John'
}, {
uid: 2,
name: 'Melinda'
}]
constructor(public presence: PresenceService) {}
}
我看到了三种解决方法:
contacts
对象中。在这种情况下,Angular的最佳做法是什么?
答案 0 :(得分:2)
我认为对您的问题的直接答案是个人喜好,将在线状态与联系信息始终分开以模板的方式以及将其加入服务的上游。话虽如此,这是我的一些想法:
将 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Users>
<User>
<Name>AAA</Name>
<Surname>AAA</Surname>
</User>
<User>
<Name>BBB</Name>
<Surname>BBB</Surname>
</User>
</Users>
更改为以下内容:
PresenceService
讨论点:
@Injectable()
export class PresenceService {
private readonly onlineUsers = new BehaviorSubject<string[]>([]);
constructor() {
// This line is written with the assumption that your websocket is wrapped
// in an Observable but by no means does it have to be.
myWebsocket.subscribe(onlineUsers => this.onlineUsers.next(this.onlineUsers))
}
isOnline(uid: string): Observable<boolean> {
return this.onlineUsers.pipe(map(users => users.includes(uid)));
}
}
从onlineUsers
更改为一个简单的字符串数组。我的理由是,如果该数组仅包含在线用户列表,则不需要存储不在线用户列表。{[s: string]: boolean}
方法是一种可观察到的方法,因此,如果在显示用户列表的组件已呈现时用户在线,则该组件将始终显示正确且最新的内容。为所有用户更新在线状态信息。通常建议不要将服务公开给模板。这意味着组件类应提供一种包装必需的服务方法的方法:
isOnline
原因是双重的。 (1.)模板不应该知道数据来自哪里,(2。)通常会使模板更简单/更混乱。
通过上述更改,您的模板现在如下所示:
isOnline(uid: string): Observable<boolean> {
return this.presenceService.isOnline(uid);
}
遍历上面讨论的区域后,我个人将联系信息及其在线状态保持分开,只要在呈现组件时仅一次检索到联系即可(其中,源可能是对组件的HTTP请求)。后端),但它们的在线状态可能会在组件的生命周期内发生变化(通过网络套接字进行更新)。我的理由是,通过这种方式,模板中的<p *ngFor="let c of contacts">
{{ c.name }} is {{ (isOnline(c.uid) | async) ? 'online' : 'offline' }}
</p>
可以在联系人之间循环一次,即使联系人的在线状态发生了变化也不必再次更改。
答案 1 :(得分:1)
我必须对您的设置进行一些假设,但是我相信这是您要寻找的:
在您的PresenceService
中,您正在发生某种网络事件。将其转换为一个Observable,例如使用fromEvent
创建功能。为简单起见,我使用Angular HTTP演示异步事件。
调整您的isOnline
函数以返回Observable,如下所示:
isOnline(uid: string): Observable<boolean> {
return this.http.get("/presence/" + uid);
}
如果您为每个联系人多次调用此函数(使用*ngFor
时可能会调用该函数),则建议将结果传递给shareReplay(1)
运算符。它将“缓存”响应。
isOnline(uid: string): Observable<boolean> {
return this.http.get("/presence/" + uid).pipe(shareReplay(1));
}
现在,在Angular模板中,使用async
管道。它订阅并随后取消订阅给定的Observable。您可以使用它来发起网络请求并接收更新。 Angular会为您处理更新通知的值。
您的模板可能如下所示:
<h3> Your contacts </h3>
<p *ngFor="let c of contacts">
{{c.name}} is {{(presence.isOnline(c.uid) | async) ? 'online' : 'offline'}}
</p>
答案 2 :(得分:1)
在这种情况下,您的两个来源都可以显示为“可观察对象”:
@Injectable()
export class PresenceService {
public onlineUsers$: Observable<{}>;
}
@Injectable()
export class ContactService {
public contacts$: Observable<Contact[]>;
}
在您的组件中,您需要将这两个Observable与rxjs粘合在一起:
@Component({
selector: 'my-app',
template: `
<h3> Your contacts </h3>
<ng-container *ngIf="contactsWithPresenceInfo$|async; let contacts">
<p *ngFor="let c of contacts">
{{c.name}} is {{c.isOnline ? 'online' : 'offline'}}
</p>
</ng-container>
`
})
export class AppComponent implements OnDestroy, OnInit {
private destroy$ = new Subject();
public contactsWithPresenceInfo$: Observable<any>;
constructor(public presenceService: PresenceService, public contactService: ContactService) {}
ngOnInit(): void {
// Attention: import combineLatest from 'rxjs', not from 'rxjs/operators'
// When combineLatest is imported from 'rxjs/operators', it can be used as an operator (passed in the pipe function)
// The one imported from 'rxjs' is the "creation" variant
this.contactsWithPresenceInfo$ = combineLatest(
this.presenceService.onlineUsers$,
this.contactService.contacts$
).pipe(
map(([presenceInfo, contacts]) => mergePresenceInfoOntoContacts(presenceInfo, contacts)),
takeUntil(this.destroy$)
);
}
mergePresenceInfoOntoContacts(presenceInfo, contacts) {
// loop over your contacts, apply the presence info
// and return them in this format:
// [{ name: '', isOnline: boolean }]
return [];
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
请紧记:CombineLatest仅在每个Observable至少发出一次值时才返回数据!这意味着,如果您的联系人是从数据库中加载的,但是尚未收到任何状态信息,则contactsWithPresenceInfo $将不返回任何内容。可以通过使用startWith来轻松解决此问题:
this.contactsWithPresenceInfo$ = combineLatest(
this.presenceService.onlineUsers$.pipe(startWith({})),
this.contactService.contacts$
).pipe(
map(([presenceInfo, contacts]) => mergePresenceInfoOntoContacts()),
takeUntil(this.destroy$)
);
此方法的优点是,对于ContactService或PresenceService的每个新响应,都将构造一个新对象(不变性!)。您将能够在组件上设置ChangeDetection.OnPush并获得一些性能,因为将触发更少的更改检测。不变性,RXJS和OnPush策略可以很好地协同工作...
我包括了一个takeUntil运算符,并传入了destroy $主题,因为这是一种习惯:对于所有已定义的rxjs语句,这都可以作为一种自动取消订阅的功能。在这种情况下,由于异步管道将为您管理取消订阅,因此这并不是必须的。