Angular 的模块间通信

913 阅读9分钟
原文链接: mp.weixin.qq.com
作者 | Sergio Cruz 译者 | 足下 模块是Angular的构建单元,Angular应用程序的所有可视化元素也是由模块构建的。当我们把模块拆散成更小的模块时,我们就要确保它们可以把数据传来传去。到那时候,恰当地模块间通信机制就成了我们应用程序的基础,可以让所有的数据都保持同步状态。

本文要点:

  • Angular是用基于模块的应用程序架构构建的,所以是有可能在模块之间传输数据的;

  • 可以通过@Input()把数据传入子模块;

  • 可以通过@Output()从子模块捕获数据;

  • 随着应用程序的发展,父子模块之间的通信会越来越困难;

  • 你可以通过ngrx之类的方法用外部数据库来构建大规模应用程序;

模块是Angular的构建单元,Angular应用程序的所有可视化元素也是由模块构建的。基于模块的架构的一个重大好处在于,与JavaScript函数非常相像,如果一块代码变得过于复杂,或者承担了过多责任,你就可以把它打散,让每一段代码都只做一件事

也就是说,当我们把模块拆散成更小的模块时,我们就要确保它们可以把数据传来传去。到那时候,恰当地模块间通信机制就成了我们应用程序的基础,可以让所有的数据都保持同步状态。幸运的是,Angular给我们提供了这样的工具来完成这件事。

如上图所示,我们可以在AppComponent之下构建整个应用程序,但这样做会让那个模块承担过多的责任。在基于模块的架构里,大家公认比较好的实践是把模块拆散,好让它们都只承担单一的责任。

将数据传给模块

在Angular中,当一个父模块要把数据传入子模块中时,我们可以使用@Input。假设我们现在要构建一个程序,在页面上显示评论。AppComment将负责加载所有的评论内容,我们会把每条评论数据都发送给评论模块。

我们将调用:

@Input() comment

把一个comment参数传入子模块。下面就是整块模块代码的样子:

@Component({
  selector: 'comment',
  templateUrl: './comment.component.html',
  styleUrls: ['./comment.component.css']
})
export class CommentComponent {
  @Input() comment;

现在我们可以从我们代码其它部分调用这个模块,并把它需要的数据传入这个模块了。这块代码看起来会像这样:

<comment [comment]="comment"></comment>
理解语法

首先,我们有一个模块选择器:

<comment></comment>

如果你以前用过Angular,这个语法看起来应该是很熟悉的。

其次是属性绑定:[comment]。这把我们元素的属性括起来的中括号第一眼看上去似乎有些令人费解。事实上,并不需要它们来帮着把数据传到各个模块中,但没有它们,我们就只能把一个普通的文本字符串传入模块的@Input()。中括号可以告诉Angular,这是一个属性绑定以及传给这个变量的值,所以这样就可以把动态数据值插入,传给各模块,而不只是传过去一个字符串了。

最后,我们在“comment”的属性绑定之后有了属性的值。这一块就是告诉Angular要注意这个“comment”属性,并且把它传给我们的评论模块。

将概念组合起来

为了像我们在上图中所显示地展示一个评论的列表,我们可以把许多Angular概念组合起来,包括*ngFor。咱们假设我们可以用一个名为this.comments的属性的方式把评论数据作为我们模块类的一部分导入。

<comment
  *ngFor="let comment of comments"
  [comment]="comment"></comment>

最终结果看起来像是这样的:

使用评论模块的评论列表

捕获子模块事件

当我们知道了该如何把数据传入评论模块之后,那该怎样把一个模块删掉,并因此从列表中消失呢?这个看起来就有些诡异,因为数据是保存在评论模块的父模块——AppComponent之中的。

解决这个问题的办法就是调用名为@Output()的方法,它可以利用EventEmitter,让子模块发射出父模块可以捕获的事件。

让子模块可以与父模块通信的第一步就是用@Output()描述符给我们的模块加上一个新的类属性。

@Component({
  selector: 'comment',
  templateUrl: './comment.component.html'
})
export class CommentComponent {

  @Input() private comment;
  @Output() private onDelete = new EventEmitter();

  deleteComment() {
    this.onDelete.emit(this.comment);
  }
}

在这个CommentComponent的代码里,我们可以在delete按钮被按下时调用deleteComment()方法,让我们可以捕获这些从父模块发过来的事件。

<button (click)="deleteComment()">Delete</button>
从父模块中捕获事件

在AppComponent中,我们需要有个新方法来处理删除评论的行为。这个方法会收到:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {

  /**[code omitted]**/

  onCommentDelete(comment) {
    // logic to remove comment from comments array
  }

}

在我们的视图中,我们只需要告诉Angular当onDelete事件被触发时,要调用onCommentDelete()方法。

<comment
      *ngFor="let comment of comments"
      [comment]="comment"
      (onDelete)="onCommentDelete($event)"></comment>

当我们的删除功能就绪之后我们的程序看起来是这样的:

现在有了Angular的@Output()描述符,我们就可以删除评论了。

还有另外一种获取数据的方法

到目前为止,我们还只讨论了父-子模块之间进行模块通信的方法。尽管现在这种通信方法已经可以满足大多数人的需求,但随着应用程序规模的增长,继续维护父子之模块间的数据通信模式就会越来越困难。对于大型程序来说,用数据库来减轻单个模块的工作量的方法通常都是有效的。数据库可以做为中心存储,在需要时可以被应用程序的各个单独模块调用。单个模块可以直接连上数据库,只使用它们所需要的那一部分数据,而不是把数据手动地顺序按子模块链不断传下去,从而减轻了父模块和子模块所承担的把数据传来传去的责任。

如果你熟悉React,就会知道这正是Redux要解决的问题。可是有了Angular,我们就有了另一个可用的名为ngrx的库,而这恰好是受Redux启发而成的。在这两个库之间有些关键不同:类型和观测值。ngrx库对TypeScript生态系统的依赖非常重,所以会比Redux更加繁冗,但却让调试和调查问题变得更容易。

在用ngrx时,我们的状态是一个单独不变的数据结构。为了让我们的状态可变,我们可以用行为的方式来调用函数。反之,这些行为可以告诉我们的由单纯的函数组成的处理逻辑,哪一块的状态是可变的。这样,我们的应用程序就可以有状态的新版本数据了,而那些关注这些数据的模块就可以马上收到新数据。当这些模块收到新数据之后,它们会被自动重新展示,让我们的视图始终与数据库中的数据保持一致。

这对于开发又意味着什么?

这从概念上表明,是有可能从系统外部把数据直接传给某个模块,而不必通过父子关系的,这在事实上对于开发来说意味着什么呢?

如果我们想在两个不同的地方显示评论的条数呢?仅仅依靠模块的层级架构是很难实现的,这种情况下使用ngrx的中心存储就很有效了。

在较大的程序内部,把数据直接传给某个模块可以解决很多问题。随着各个模块要处理越来越多的任务,再要增加类似跟踪把数据传入子模块这类额外的任务就会变得复杂。当不能把传输数据的负担交给外部数据库时,维护大型Angular程序时保持头脑清醒就很必要了。

在上面的例子里,一个外部数据库(图中没有画出来)可以用来把信息传给SidebarComponent,表示有多少评论可用,“有两条评论”,同时还把真实的评论内容发给CommentList模块。这些数据完全绕过了父模块AppComponent。

快速回顾Angular的模块通信机制

读过上文,我们已经对Angular的模块之间如何通信有了一些了解。我们知道了怎样把数据从一个父模块传入一个子模块,而且我们也知道了怎么使用Angular的@Input()描述符。

我们也知道了怎样使用@Output()描述符,以及如何使用EventEmitter来把数据从一个子模块发回给一个父模块。

除此之外,当应用程序规模增长之后,我们也知道了如何用ngrx来减轻父子关系的压力。通过使用一个单独的数据库,我们就可以让各个模块自己获取数据,而不必通过多层子模块居中传递来把数据传到真正的目的地去。这就让维护大规模应用程序变得容易了。这些原理也适用于React之类的库,他们也用非常类似的办法解决了相同的问题。

本文相关链接

  • 演示程序:

    http://https//sergiocruz.github.io/ng-sample-comments/

  • 相应代码:

    https://github.com/sergiocruz/ng-sample-comments

  • 阅读英文原文请移步:

    www.infoq.com/articles/an…

作者介绍

Sergio Cruz,是Code School的一位应用程序开发者和咨询师,兴趣广泛,尤其是JavaScript。最近,他新讲授了一些Code School的React课程,“ Powering up With React”。当他不在Code School敲代码时,他也会去ng-conf或OSCON等JavaScript相关的会议上作作讲座。