kittencup / angular2-ama-cn

angular2 随便问
691 stars 101 forks source link

基于Observables的数据架构 - 第2部分:视图组件 #41

Open kittencup opened 8 years ago

kittencup commented 8 years ago

该issue关闭讨论,如有问题请去 https://github.com/kittencup/angular2-ama-cn/issues/43 提问

目录

kittencup commented 8 years ago

建立我们的视图:顶层组件ChatApp

让我们将注意力转移到我们的应用程序,并实现我们的视图组件。

为了清晰和空间,在下面的章节中,我将要离开1。import语句,CSS,和其他一些不重要的东西。如果您对这些每一行细节好奇的话,打开示例代码,因为它包含了我们应用需要运行的所有代码。

我们要做的第一件事就是创建我们的顶层组件chat-app

正如我们前面谈到的,页面被分为三个顶层组件:

image

下面是我们的组件代码的样子:

# code/rxjs/chat/app/ts/app.ts

@Component({
  selector: "chat-app"
}) @View({
  directives: [ChatNavBar,
               ChatThreads,
               ChatWindow],
template: `
<div> <nav-bar></nav-bar> <div class="container">
      <chat-threads></chat-threads>
      <chat-window></chat-window>
    </div>
</div>
` })
class ChatApp {
  constructor(public messagesService: MessagesService,
              public threadsService: ThreadsService,
              public userService: UserService) {
    ChatExampleData.init(messagesService, threadsService, userService);
} }
bootstrap(ChatApp, [ servicesInjectables, utilInjectables ]);

看一看构造函数,在这里我们注入了3个服务,MessagesService, ThreadsService, 和 UserService 我们使用这些服务来初始化我们的示例数据。

如果你对例子数据感兴趣的,你可以在这找到它code/rxjs/chat/app/ts/ChatExampleData.ts

kittencup commented 8 years ago

ChatThreads组件

接下来让我们在chatThreads组件建立我们的Thread列表。

image

我们的selector很简单,我们想要匹配chat-threads的选择器

# code/rxjs/chat/app/ts/components/ChatThreads.ts

@Component({
  selector: "chat-threads"
})

ChatThreads控制器

看看我们的组件控制器ChatThreads:

# code/rxjs/chat/app/ts/components/ChatThreads.ts
export class ChatThreads {
  threads: Observable<any>;

  constructor(public threadsService: ThreadsService) {
    this.threads = threadsService.orderedThreads;
  }
}

在这里,我们注入ThreadsService然后我们保持一个orderedThreads的引用。

ChatThreads 模板

最后,让我们来看看模板配置

# code/rxjs/chat/app/ts/components/ChatThreads.ts

@Component({
    selector: 'chat-threads',
    directives: [ChatThread],
    changeDetection: ChangeDetectionStrategy.OnPushObserve,
    template: `
    <!-- conversations -->
<div class="row">
<div class="conversation-wrap">
        <chat-thread
             *ngFor="#thread of threads | async"
[thread]="thread"> </chat-thread>
      </div>
    </div>
`
})

来看看这里东西:带有async管道的NgFor,ChatThread和ChangeDetectionStrategy。

该ChatThread指令组件(相匹配的chat-thread标记)将显示该Threads的视图。这个组件是我们在上面定义的。

NgFor迭代我们的threads,并传入属性[thread]给我们的ChatThread指令。

但是,你可能会注意到在我们的*ngFor里的新东西:一个叫async管道

RxPipe是定义这种行为的类。RxPipe是一个自定义,管道,它让我们在视图中使用RxJS的Observable.我们不打算花太多时间谈论RxPipe的实现细节,但你可以在/rxjs/app/ts/util/RxPipe.t找到定义的代码

async是通过AsyncPipe实现的,它可以让我们在模板中使用RxJS的Observable,这是非常棒的,因为Angular2让我们使用异步Observable就好像是同步集合。这是超级方便,真的很酷。

在这部分,我们指定一个自定义changeDetection。 Angular2具有灵活,高效的变化检测系统。其中一个好处是,如果我们组件有不可变的或可观察的绑定,那么我们能够得到变化检测系统的提示,这将使我们的应用程序的运行效率更高。

In this case, instead of watching for changes on an array of Threads, Angular will subscribe for changes to the threads observable - and trigger an update when a new event is emitted. =============这里不翻译了,.OnPushObserve已经弃用了=============

下面是我们的总的ChatThreads组件的样子:

# code/rxjs/chat/app/ts/components/ChatThreads.ts

@Component({
  selector: 'chat-threads',
  directives: [ChatThread],
  changeDetection: ChangeDetectionStrategy.OnPushObserve,
  template: `
    <!-- conversations -->
    <div class="row">
      <div class="conversation-wrap">

        <chat-thread
             *ngFor="#thread of threads | async"
             [thread]="thread">
        </chat-thread>

      </div>
    </div>
  `
})
export class ChatThreads {
  threads: Observable<any>;

  constructor(public threadsService: ThreadsService) {
    this.threads = threadsService.orderedThreads;
  }
}
kittencup commented 8 years ago

单个ChatThread组件

让我们看一下我们的ChatThread组件。这将用于显示单个Thread。首先是@Component:

# code/rxjs/chat/app/ts/components/ChatThreads.ts

@Component({
  inputs: ['thread'],
  selector: 'chat-thread',

ChatThread控制器和ngOnInit

在这里,当我们的组件在第一次被检查变化后会调用onInit.

让我们来看看组件控制器:

# code/rxjs/chat/app/ts/components/ChatThreads.ts

class ChatThread implements OnInit {
    thread:Thread;
    selected:boolean = false;

    constructor(public threadsService:ThreadsService) {
    }

    ngOnInit():void {
        this.threadsService.currentThread
            .subscribe((currentThread:Thread) => {
                this.selected = currentThread &&
                    this.thread &&
                    (currentThread.id === this.thread.id);
            });
    }

    clicked(event:any):void {
        this.threadsService.setCurrentThread(this.thread);
        event.preventDefault();
    }
}

注意在这里我们实现了一个接口:OnInit,Angular 组件有很多生命周期事件,我们会在组件这章讨论更多关于生命周期事件

在这里,因为我们实现了OnInit接口,ngOnInit方法将会在组件在第一次被变化检测后调用。

我们使用ngOnInit的一个重要原因是因为我们的thread属性将不提供在构造函数中使用

上面可以看到,在我们的ngOnInit订阅threadsService.currentThread如果currentThread与此组件的thread id相匹配,我们设置选择为true(相反,如果thread不匹配,我们设置选择为false)。

我们还要设置一个点击事件处理,这就是我们如何处理选择当前thread的方式。在我们@View下面,我们会在thread视图上绑定一个click()。如果我们接受到clicked(),那么我们告诉threadsService来设置当前Thread为此Thread

ChatThread 模板

下面是我们的模板

# code/rxjs/chat/app/ts/components/ChatThreads.ts

@Component({
    inputs: ['thread'],
    selector: 'chat-thread',
    template: `
        <div class="media conversation">
            <div class="pull-left">
                <img class="media-object avatar"
                     src="{{thread.avatarSrc}}"></div>
            <div class="media-body">
                <h5 class="media-heading contact-name">{{thread.name}}
                    <span *ngIf="selected">&bull;</span></h5>
                <small class="message-preview">{{thread.lastMessage.text}}</small>
            </div>
            <a (click)="clicked($event)" class="div-link">Select</a>
        </div>
`
})

请注意,我们已经有了像一些直接的绑定{{thread.avatarSrc}},{{thread.name}}和{{thread.lastMessage.text}}。

我们有一个*ng-if, 如果选择了这个threads,将会显示一个&bull

最好,我们绑定了(click)事件到我们的clicked()处理程序上,注意,当我们调用clicked时我们会传递一个$event参数,这是Angular提供的一个特殊变量,它描述了event,我们在我们的clicked处理程序中调用了event.preventDefault().这可以确保我们不会导航到不同的页面。

ChatThread完整的代码

这里是整个ChatThread组件代码

import {
  Component,
  OnInit,
  ChangeDetectionStrategy
} from 'angular2/core';
import {ThreadsService} from '../services/services';
import {Observable} from 'rxjs';
import {Thread} from '../models';

@Component({
  inputs: ['thread'],
  selector: 'chat-thread',
  template: `
  <div class="media conversation">
    <div class="pull-left">
      <img class="media-object avatar" 
           src="{{thread.avatarSrc}}">
    </div>
    <div class="media-body">
      <h5 class="media-heading contact-name">{{thread.name}}
        <span *ngIf="selected">&bull;</span>
      </h5>
      <small class="message-preview">{{thread.lastMessage.text}}</small>
    </div>
    <a (click)="clicked($event)" class="div-link">Select</a>
  </div>
  `
})
class ChatThread implements OnInit {
  thread: Thread;
  selected: boolean = false;

  constructor(public threadsService: ThreadsService) {
  }

  ngOnInit(): void {
    this.threadsService.currentThread
      .subscribe( (currentThread: Thread) => {
        this.selected = currentThread &&
          this.thread &&
          (currentThread.id === this.thread.id);
      });
  }

  clicked(event: any): void {
    this.threadsService.setCurrentThread(this.thread);
    event.preventDefault();
  }
}
kittencup commented 8 years ago

ChatWindow组件

该ChatWindow是我们的应用程序中最复杂的部分。让我们逐一看这部分:

image

首先来定义我们的@Component

# code/rxjs/chat/app/ts/components/ChatWindow.ts

@Component({
  selector: 'chat-window',
  directives: [ChatMessage,
               FORM_DIRECTIVES],
  changeDetection: ChangeDetectionStrategy.OnPushObserve,

ChatWindow属性

我们的ChatWindow有4个属性

# code/rxjs/chat/app/ts/components/ChatWindow.ts

export class ChatWindow { 
    messages: Rx.Observable<any>; 
    currentThread: Thread; 
    draftMessage: Message;
    currentUser: User;

以下图是其中每一个组件组成部分:

image

在我们的构造函数中注入4样东西

# code/rxjs/chat/app/ts/components/ChatWindow.ts

constructor(public messagesService: MessagesService,
              public threadsService: ThreadsService,
              public userService: UserService,
              public el: ElementRef) {
  }

前3个是我们服务。第4个 el是一个ElementRef,我们可以用它来获得host DOM元素。当我们创建和接受新消息时我们使用它滚动到聊天窗口的底部

记住: 在构造函数中使用public messagesService: MessagesService,我们不仅注入MessagesService并建立一个实例变量,我们也可以在后面在类中通过this.messagesService使用

ChatWindow onInit

我们要把这部分在NgOnInit初始化.我们将在这里主要是设置订阅我们的Observable来改变我们的组件属性。

# code/rxjs/chat/app/ts/components/ChatWindow.ts

NgOnInit(): void {
    this.messages = this.threadsService.currentThreadMessages;
    this.draftMessage = new Message();

首先,我们将currentThreadMessages保存到messages属性,接下去我们创建一个空的Message来初始化默认的draftMessage

当我们发送一个新的消息时,我们需要确保Messages保存一个发送的Thread的引用,发送的thread将一直是当前thread,所以让我们存储一个引用到当前选定的thread:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

    this.threadsService.currentThread.subscribe( (thread: Thread) => {
    this.currentThread = thread; 
});

我们也希望从当前用户发送新邮件,让我们用currentUser做同样的事:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

this.userService.currentUser
      .subscribe(
        (user: User) => {
          this.currentUser = user;
});

ChatWindow sendMessage

既然我们在讨论它,那么让我们来实现一个SendMessage函数用来发送一个新的信息:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

sendMessage(): void {
    let m: Message = this.draftMessage;
    m.author = this.currentUser;
    m.thread = this.currentThread;
    m.isRead = true;
    this.messagesService.addMessage(m);
    this.draftMessage = new Message();
}

sendMessage上面会产生draftMessage,使用我们的组件属性设置author和thread,每一个消息我们发送并"已阅读"(我们编写的),所以我们给他们打上已读标记

注意在这里我们没有更新draftMessage保存的消息文字,这是因为在稍后我们会在视图中为消息绑定值

之后,我们已经更新了draftMessage属性,我们把它交给messageService,并且我们为draftMessage创建了一个Message,我们这样做是为了确保我们不会改变已经发送的消息。

ChatWindow onEnter

在我们的视图中,在2种情况下我们要发送消息 1.用户点击"发送"按钮 2.用户敲击Enter(或Return)按键

让我们来定义一个处理这个事件的函数

# code/rxjs/chat/app/ts/components/ChatWindow.ts

onEnter(event: any): void {
  this.sendMessage();
  event.preventDefault();
}

ChatWindow scrollToBottom

当我们发送一个消息,或当有一个新的消息近来,我们在这个聊天窗口中滚动到底部,要做到这点,在我们的host元素上设置scrollTop属性

# code/rxjs/chat/app/ts/components/ChatWindow.ts

scrollToBottom(): void {
  let scrollPane: any = this.el
    .nativeElement.querySelector(".msg-container-base");
  scrollPane.scrollTop = scrollPane.scrollHeight;
}

现在我们有一个函数用来滚动到底部,我们必须保证在正确的时候调用该函数,早在NgOnInit,让我们订阅currentThreadMessages的列表,任何时候我们得到一个新消息就滚动到底部

# code/rxjs/chat/app/ts/components/ChatWindow.ts

this.messages
      .subscribe(
        (messages: Array<Message>) => {
          setTimeout(() => {
            this.scrollToBottom();
          });
        });

为什么会有setTimeout? 当我们获取一个新的消息,如果我们立即调用scrollToBottom,那么会滚动到底部会发生在新消息渲染中之前,使用setTimeout是告诉javascript我们想要在这次执行队列完成后运行该函数,这发生在组件的渲染后,所以它是我们想要的。

ChatWindow 模板

template应该看起来很熟悉,首先我们定义了一些标记和header面板:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

@Component({
  selector: 'chat-window',
  directives: [ChatMessage,
               FORM_DIRECTIVES],
  changeDetection: ChangeDetectionStrategy.OnPushObserve,
  template: `
<div class="chat-window-container">
    <div class="chat-window">
        <div class="panel-container">
            <div class="panel panel-default">
                <div class="panel-heading top-bar">
                    <div class="panel-title-container">
                        <h3 class="panel-title">
                            <span class="glyphicon glyphicon-comment"></span> Chat - {{currentThread.name}}
                        </h3></div>
                    <div class="panel-buttons-container">
                        <!-- you could put minimize or close buttons here -->
                    </div>
                </div>

接下去 我们显示消息列表,在这里我们使用了ngFor与async管道来迭代我们的消息列表,我们将描述下个人chat-message组件

# code/rxjs/chat/app/ts/components/ChatWindow.ts

<div class="panel-body msg-container-base">
  <chat-message
       *ng-for="#message of messages | async"
       [message]="message">
  </chat-message>
</div>

最后,我们有消息输入框和关闭标签:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

<div class="panel-footer">
              <div class="input-group">
                <input type="text" 
                       class="chat-input"
                       placeholder="Write your message here..."
                       (keydown.enter)="onEnter($event)"
                       [(ngModel)]="draftMessage.text" />
                <span class="input-group-btn">
                  <button class="btn-chat"
                     (click)="onEnter($event)"
                     >Send</button>
                </span>
              </div>
            </div>

          </div>
        </div>
      </div>
    </div>

消息输入框是这个视图中最有趣的部分,所以让我们来谈谈几件事情.

我们的input标签有2个有趣的属性: 1.(keydown.enter) 和 2.[(ngModel)]

处理按键

Angular 2提供了一个简单的方式来处理键盘操作:我们绑定事件在一个元素上,在这个例子中,我们绑定了Keydown.enter,这表示如果敲击"Enter",则调用表达式里的函数,这种情况下是onEnter($event)函数

# code/rxjs/chat/app/ts/components/ChatWindow.ts

 <input type="text" 
                       class="chat-input"
                       placeholder="Write your message here..."
                       (keydown.enter)="onEnter($event)"
                       [(ngModel)]="draftMessage.text" />

使用ngModel

正如我们之前所说的,Angular没有一个通用的双向绑定模型,然后,它可以在组件和视图之间有一个非常有用双向绑定,它可以是一个非常方便的方式来保持一个组件属性与视图同步。

在这种情况下,我们在input标签的值和draftMessage.text之间建立了双向绑定,就是说,如果在input标签中输入,draftMessage.text会自动设置这个input的值,同样,如果在我们代码中我们更新draftMessage.text,在视图中的input标签的值也会改变。

# code/rxjs/chat/app/ts/components/ChatWindow.ts

 <input type="text" 
                       class="chat-input"
                       placeholder="Write your message here..."
                       (keydown.enter)="onEnter($event)"
                       [(ngModel)]="draftMessage.text" />

点击 "发送"

在我们的"Send"按钮,我们绑定(click)属性到我们组件中的onEnter函数

# code/rxjs/chat/app/ts/components/ChatWindow.ts

<span class="input-group-btn"> 
  <button class="btn-chat" (click)="onEnter($event)">Send</button> 
</span>

完整的chatwindow组件

下面是整个ChatWindow组件的完整的代码:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

@Component({
  selector: 'chat-window',
  directives: [ChatMessage,
               FORM_DIRECTIVES],
  changeDetection: ChangeDetectionStrategy.OnPushObserve,
  template: `
    <div class="chat-window-container">
      <div class="chat-window">
        <div class="panel-container">
          <div class="panel panel-default">

            <div class="panel-heading top-bar">
              <div class="panel-title-container">
                <h3 class="panel-title">
                  <span class="glyphicon glyphicon-comment"></span>
                  Chat - {{currentThread.name}}
                </h3>
              </div>
              <div class="panel-buttons-container">
                <!-- you could put minimize or close buttons here -->
              </div>
            </div>

            <div class="panel-body msg-container-base">
              <chat-message
                   *ngFor="#message of messages | async"
                   [message]="message">
              </chat-message>
            </div>

            <div class="panel-footer">
              <div class="input-group">
                <input type="text" 
                       class="chat-input"
                       placeholder="Write your message here..."
                       (keydown.enter)="onEnter($event)"
                       [(ngModel)]="draftMessage.text" />
                <span class="input-group-btn">
                  <button class="btn-chat"
                     (click)="onEnter($event)"
                     >Send</button>
                </span>
              </div>
            </div>

          </div>
        </div>
      </div>
    </div>
  `
})
export class ChatWindow implements OnInit {
  messages: Observable<any>;
  currentThread: Thread;
  draftMessage: Message;
  currentUser: User;

  constructor(public messagesService: MessagesService,
              public threadsService: ThreadsService,
              public userService: UserService,
              public el: ElementRef) {
  }

  ngOnInit(): void {
    this.messages = this.threadsService.currentThreadMessages;

    this.draftMessage = new Message();

    this.threadsService.currentThread.subscribe(
      (thread: Thread) => {
        this.currentThread = thread;
      });

    this.userService.currentUser
      .subscribe(
        (user: User) => {
          this.currentUser = user;
        });

    this.messages
      .subscribe(
        (messages: Array<Message>) => {
          setTimeout(() => {
            this.scrollToBottom();
          });
        });
  }

  onEnter(event: any): void {
    this.sendMessage();
    event.preventDefault();
  }

  sendMessage(): void {
    let m: Message = this.draftMessage;
    m.author = this.currentUser;
    m.thread = this.currentThread;
    m.isRead = true;
    this.messagesService.addMessage(m);
    this.draftMessage = new Message();
  }

  scrollToBottom(): void {
    let scrollPane: any = this.el
      .nativeElement.querySelector('.msg-container-base');
    scrollPane.scrollTop = scrollPane.scrollHeight;
  }

}
kittencup commented 8 years ago

ChatMessage组件

image

这个组件相对简单,这里的主要是逻辑是如果消息是由当前用户编写的渲染一个稍微不同的视图。如果消息不是当前用户编写的,我们要考虑消息的传入

我们开始定义@Compoent:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

@Component({
  inputs: ['message'],
  selector: 'chat-message',
  pipes: [FromNowPipe],
})

设置incoming

记住每个ChatMessage属于一个Message.所以在onInit里我们订阅currentUser流,并取决于该消失是否是当前用户编写的来设置incoming

# code/rxjs/chat/app/ts/components/ChatWindow.ts

export class ChatMessage implements OnInit {
    message:Message;
    currentUser:User;
    incoming:boolean;

    constructor(public userService:UserService) {
    }

    ngOnInit():void {
        this.userService.currentUser
            .subscribe(
                (user:User) => {
                    this.currentUser = user;
                    if (this.message.author && user) {
                        this.incoming = this.message.author.id !== user.id;
                    }
                });
    }
}

ChatMessage 模板

在我们的模板有2个有趣的东西:

  1. FromNowPipe
  2. [ngClass]

首先,这里的代码:

# code/rxjs/chat/app/ts/components/ChatWindow.ts

@Component({
  inputs: ['message'],
  selector: 'chat-message',
  pipes: [FromNowPipe],
  template: `
  <div class="msg-container"
       [ngClass]="{'base-sent': !incoming, 'base-receive': incoming}">

    <div class="avatar"
         *ngIf="!incoming">
      <img src="{{message.author.avatarSrc}}">
    </div>

    <div class="messages"
      [ngClass]="{'msg-sent': !incoming, 'msg-receive': incoming}">
      <p>{{message.text}}</p>
      <time>{{message.sender}} • {{message.sentAt | fromNow}}</time>
    </div>

    <div class="avatar"
         *ngIf="incoming">
      <img src="{{message.author.avatarSrc}}">
    </div>
  </div>
  `
})

该FromNowPipe是一个管道,它将我们消息的发送的时间转化为人类可读的"x seconds ago"。你可以看到我们这样使用它{{message.sentAt | fromNow}}

FromNowPipe使用的是优秀的 moment.js 库。如果你想学习关于如果创建自定义的pipes,可以查看[Pipes]()这章。你也可以在code/rxjs/chat/app/ts/util/FromNowPipe.ts阅读FromNowPipe源码

我们也在视图中大量的使用了ngClass,我们的想法是,当我们说:

[ng-class]="{'msg-sent': !incoming, 'msg-receive': incoming}"

我们告诉angular如果incoming是true(msg-sent类是incomine为false时)则接受mesg-receive类

当使用incoming属性,我们已不同的方式来显示传入和传出消息。

完整ChatMessage代码

在这是我们完成的ChatMessage组件

# code/rxjs/chat/app/ts/components/ChatWindow.ts

import {
  Component,
  OnInit,
  ElementRef,
  ChangeDetectionStrategy
} from 'angular2/core';
import {FORM_DIRECTIVES} from 'angular2/common';
import {
  MessagesService,
  ThreadsService,
  UserService
} from '../services/services';
import {FromNowPipe} from '../util/FromNowPipe';
import {Observable} from 'rxjs';
import {User, Thread, Message} from '../models';

@Component({
  inputs: ['message'],
  selector: 'chat-message',
  pipes: [FromNowPipe],
  template: `
  <div class="msg-container"
       [ngClass]="{'base-sent': !incoming, 'base-receive': incoming}">

    <div class="avatar"
         *ngIf="!incoming">
      <img src="{{message.author.avatarSrc}}">
    </div>

    <div class="messages"
      [ngClass]="{'msg-sent': !incoming, 'msg-receive': incoming}">
      <p>{{message.text}}</p>
      <time>{{message.sender}} • {{message.sentAt | fromNow}}</time>
    </div>

    <div class="avatar"
         *ngIf="incoming">
      <img src="{{message.author.avatarSrc}}">
    </div>
  </div>
  `
})
export class ChatMessage implements OnInit {
  message: Message;
  currentUser: User;
  incoming: boolean;

  constructor(public userService: UserService) {
  }

  ngOnInit(): void {
    this.userService.currentUser
      .subscribe(
        (user: User) => {
          this.currentUser = user;
          if (this.message.author && user) {
            this.incoming = this.message.author.id !== user.id;
          }
        });
  }

}
kittencup commented 8 years ago

ChatNavBar组件

我们要讨论的最后一个组件是ChatNavBar,在nav-bar上我们要显示用户未读消息数量

image

尝试的未读邮件数的最好方法是使用"Waiting Bot"。如果你还没有未读邮件,尝试发送“3”到Waiting BOT然后切换到另一个窗口,Waiting BOT将等待3秒之后送你一个消息,你会看到未读邮件计数器增加。

ChatNavBar @Component

首先,我们定义一个相当简单的@Component

# code/rxjs/chat/app/ts/components/ChatNavBar.ts

@Component({
  selector: "nav-bar"

ChatNavBar控制器

ChatNavBar控制器需要做的唯一一件事是跟踪unreadMessagesCount。这比表面看起来要复杂得多。

最直接的方法是简单地监听messagesService.messages,并计算Message里属性isRead为false的总数,这对于当前Thread之外的所有消息都可以正常工作,然而在当前Thread里的新消息不能保证在messages发射出新的值时打上read标记。

处理这种情况最安全的方式是结合messages和currentThread流,确保我们不计算当前Thread的任何消息。

我们使用的combineLatest操作,我们已经在之前使用过了:

# code/rxjs/chat/app/ts/components/ChatNavBar.ts

export class ChatNavBar {
  unreadMessagesCount: number;

  constructor(public messagesService: MessagesService,
              public threadsService: ThreadsService) {
  }

  onInit(): void {
    this.messagesService.messages
      .combineLatest(
        this.threadsService.currentThread,
        (messages: Message[], currentThread: Thread) =>
          [currentThread, messages] )

      .subscribe(([currentThread, messages]: [Thread, Message[]]) => {
        this.unreadMessagesCount =
          _.reduce(
            messages,
            (sum: number, m: Message) => {
              let messageIsInCurrentThread: boolean = m.thread &&
                currentThread &&
                (currentThread.id === m.thread.id);
              if (m && !m.isRead && !messageIsInCurrentThread) {
                sum = sum + 1;
              }
              return sum;
            },
            0);
      });
  }
}

如果你不是TypeScript专家,你可能会发现上面的语法有点难解析,在combineLatest我们返回有currentThread和messages的2个元素数组素。

然后我们订阅该流,我们调用函数解构这些对象。接下去我们对message使用reduce来计算未读和不在当前线程的消息数。

ChatNavBar 模板

在我们的视图,我们剩下的唯一要做的是显示我们的unreadMessagesCount:

# code/rxjs/chat/app/ts/components/ChatNavBar.ts

@Component({
    selector: 'nav-bar',
    template: `
    <nav class="navbar navbar-default">
        <div class="container-fluid">
            <div class="navbar-header">
                <a class="navbar-brand" href="https://ng-book.com/2">
                    <img src="${require('images/logos/ng-book-2-minibook.png')}"/>
                    ng-book 2
                </a></div>
            <p class="navbar-text navbar-right">
                <button class="btn btn-primary" type="button">
                    Messages <span class="badge">{{unreadMessagesCount}}</span></button>
            </p>
        </div>
    </nav>
`
})

完成的ChatNavBar代码

下面是完整的ChatNavBar代码:

# code/rxjs/chat/app/ts/components/ChatNavBar.ts

import {Component, OnInit} from 'angular2/core';
import {MessagesService, ThreadsService} from '../services/services';
import {Message, Thread} from '../models';
import * as _ from 'underscore';

@Component({
  selector: 'nav-bar',
  template: `
  <nav class="navbar navbar-default">
    <div class="container-fluid">
      <div class="navbar-header">
        <a class="navbar-brand" href="https://ng-book.com/2">
          <img src="${require('images/logos/ng-book-2-minibook.png')}"/>
           ng-book 2
        </a>
      </div>
      <p class="navbar-text navbar-right">
        <button class="btn btn-primary" type="button">
          Messages <span class="badge">{{unreadMessagesCount}}</span>
        </button>
      </p>
    </div>
  </nav>
  `
})
export class ChatNavBar implements OnInit {
  unreadMessagesCount: number;

  constructor(public messagesService: MessagesService,
              public threadsService: ThreadsService) {
  }

  ngOnInit(): void {
    this.messagesService.messages
      .combineLatest(
        this.threadsService.currentThread,
        (messages: Message[], currentThread: Thread) =>
          [currentThread, messages] )

      .subscribe(([currentThread, messages]: [Thread, Message[]]) => {
        this.unreadMessagesCount =
          _.reduce(
            messages,
            (sum: number, m: Message) => {
              let messageIsInCurrentThread: boolean = m.thread &&
                currentThread &&
                (currentThread.id === m.thread.id);
              if (m && !m.isRead && !messageIsInCurrentThread) {
                sum = sum + 1;
              }
              return sum;
            },
            0);
      });
  }
}
kittencup commented 8 years ago

总结

如果我们把他们放在一起我们就有一个功能齐全的聊天应用程序!

image

如果你签出 /rxjs/chat/app/ts/ChatExampleData.ts 代码,你会看到,我们已经为你写了几个机器人,可以用来聊天的。下面是Reverse Bot的代码摘录:

let rev: User = new User("Reverse Bot", require("images/avatars/female-avatar-4.png"));
let tRev: Thread = new Thread("tRev", rev.name, rev.avatarSrc);
# code/rxjs/chat/app/ts/ChatExampleData.ts

 messagesService.messagesForThreadUser(tRev, rev)
      .forEach( (message: Message) => {
        messagesService.addMessage(
          new Message({
            author: rev,
            text: message.text.split("").reverse().join(""),
            thread: tRev
          })
        );
      });

上面可以看到,我们已经通过messages- ForThreadUser订阅了“Reverse Bot”的消息。试着写你自己的一些机器人。

kittencup commented 8 years ago

下一步

在RxJS一些方法来改善这个聊天应用程序将会变得更强,然后把它挂到一个实际的API。 我们将在HTTP这章讨论如何使用API请求。现在,享受您的喜欢聊天应用程序!