具有混合数据源的角度模板

时间:2018-11-01 10:59:55

标签: angular rxjs

我不确定哪种首选的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) {}
}

我看到了三种解决方法:

  • PresenceService ,在模板中直接调用方法“ isOnline”,如上面的代码片段所示。
  • IsOnline 管道返回存储在PresenceService中的信息。
  • 侦听组件中的状态信息,然后将未存储在数据库中的临时状态属性添加到contacts对象中。

在这种情况下,Angular的最佳做法是什么?

3 个答案:

答案 0 :(得分:2)

我认为对您的问题的直接答案是个人喜好,将在线状态与联系信息始终分开以模板的方式以及将其加入服务的上游。话虽如此,这是我的一些想法:

PresenceService

<?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

讨论点:

  1. (不是很重要),我将@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更改为一个简单的字符串数组。我的理由是,如果该数组仅包含在线用户列表,则不需要存储不在线用户列表。
  2. (这很重要){[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语句,这都可以作为一种自动取消订阅的功能。在这种情况下,由于异步管道将为您管理取消订阅,因此这并不是必须的。