Angular2中的通讯方式

Angular 2 中,我们难免需要进行组件间的相互通信,但是这些通信方式你真的都知道吗?

软件工程中,随着应用规模的不断扩大,必然需要进行 Logic Separation。在 Web 开发中,组件化和模块化的观念已经被越来越多的人所熟知,从而编写出更高质量的代码。

同时,随着实体职责的分离,我们也就会不可避免地需要进行实体间的相互通信,因为我们的应用仍然需要作为一个整体存在。因此,在本文中,将对 Angular 2 中的实体间通信方式进行简要介绍,以帮助读者编写更易于维护的代码。

术语表

  • 输入/Input:组件中的外部输入,通常由 @Input() 属性装饰器或 @Component() 类装饰器参数中的 inputs 属性指定。
  • 数据/Data(Datum):信息本身或其直接载体,后者通常为基本类型或其他直接携带信息的实体类型的实例。为可数名词,通常使用其复数形式。
  • 材料/Material:所有由 Provider 所产生的具体内容,如通过 useClass 注册并生成的 Service 的实例等。
  • 提供商/Provider:用于产生某种 Material 的实体。使用 useClass 时,Provider 通常为 Material 的类;使用 useFactory 时,Provider 通常为返回 Material 的函数;使用 useValue 时,Provider 通常为 Material 本身。其中,通过 useClass 方式注册的 Provider 通常使用 @Injectable() 装饰器修饰。

通信方式介绍

下面列出一些常用的通信方式并进行简要说明。

输入:数据

  • 通信源:父组件与子组件
  • 数据方向:父组件 => 子组件
  • 信号方向:父组件 => 子组件

数据输入是最为常用的通信方式,对于父组件与子组件/指令而言,由子指令配置所需的输入项,默认为必须,可由 @Optional() 装饰器配置为可选。同时在父组件模版中使用属性绑定语法(使用 [prop]=“expression” 绑定到表达式或 prop=“literal” 绑定到字面值)指定绑定源。随后,于子组件/指令的构造函数与 OnInit 生命周期之间,子组件/指令的输入属性绑定完成。每当绑定源发生变化时,子组件/指令的输入属性也会发生对应变化。例如:

@Component({
  template: `
    <child [propOne]="1 + 1" propTwo="1 + 1"></child>
  `
})
class Parent { }

@Component({
  selector: ‘child‘
})
class Child implements OnInit {
  @Input() propOne: number
  @Input() propTwo: string

  ngOnInit(): void {
    console.log(this.propOne) // 2
    console.log(this.propTwo) // "1 + 1"
  }
}

@Directive({
  selector: ‘[propOne][propTwo]‘
})
class Spy implements OnInit {
  @Input() propOne: number
  @Input() propTwo: string

  ngOnInit(): void {
    console.log(this.propOne) // 2
    console.log(this.propTwo) // "1 + 1"
  }
}

这里可以看到,每个组件/指令都可以定义自己所需的输入,对于同宿主的若干个指令(或一个组件和若干指令,同一个宿主不可以出现多个组件)如果有同名的输入会被共享。

另外,ng2 中的输入(属性绑定)在某种意义上来说是 “强类型” 的,拥有严格的检查机制,如果使用了一个不存在的输入会被视为语法错误(如果同时使用了原生的 CustomElements 或其他库来扩展 HTML 则需要在模块定义中注册自定义扩展语法)。

同时,在默认的变化监测策略中并且没有主动调用 changeDetector 的相关状态修改方法时,输入是动态绑定的,即一旦数据源发生变化就会对目标组件/指令的对应属性重新赋值。

通过自 ES6 引入的 GetterSetter 语法(ES5 中 Object#defineProperty 的语法糖),我们可以很方便地在每次输入变化时得到通知:

@Component({
  selector: ‘child‘
})
class Child implements OnInit {
  @Input() set propOne(value: number) {
    console.log(`Property one changed to ${value}`)
  }

  // ...
}

但是我们这里发现了一个问题,如果输入属性的值没有变化的话,我们又想要通知到目标(子)组件/指令,那要怎么办呢?事实上,由于 Angular 2 采用脏检测的机制,我们并没有办法直接应用一个 “变化后的值与变化前相同” 的变化。诚然,我们可以对绑定的数据进行一层封装,然后绑定封装对象,但有一些时候,我们并不想传递什么数据,只是需要单纯地传递一个信号,即一个 Void Input,这时候,除了使用封装对象,我们还可以有另一些方式供选择。

输入:事件

  • 通信源:父组件与子组件
  • 数据方向:父组件 => 子组件(可空)
  • 信号方向:父组件 => 子组件

我们已经知道(后文中也会提到),输出属性(事件绑定)使用了 Observable 这个事件流来实现下级到上级的信号传递。和输入属性不同,输出属性的 “变化” 不依赖于脏检测,而是基于主动的事件通知。

事实上,对输入属性,我们也同样可以使用事件流来绑定我们传递输入内容:

@Component({
  selector: ‘child‘
})
class Child implements OnInit, OnDestroy {
  @Input() set propOne(value: Observable<number>) {
    if (this.propOneSubscription) {
      this.propOneSubscription.unsubscribe()
    }

    this.propOneSubscription = value.subscribe(/* Some Logic */)
  }

  private propOneSubscription: Subscription<number>

  ngOnDestroy(): void {
    this.propOneSubscription.unsubscribe()
  }

  //...
}

当然,由于不像输出属性那样由 ng 自动管理,因此我们需要自行管理订阅,以免产生内存泄漏。

不过,我们可以借助 AsyncPipe,其中已经封装好了对 Observable 生命周期的管理,只需要在模版中指定即可。

@Component({
  selector: ‘child‘,
  template: `<p>{{ propOne | async }}</p>`
})
class Child implements OnInit, OnDestroy {
  @Input() propOne: Observable<number>
}

相比于直接的数据输入而言,事件流输入更有利于对组件内部状态的控制。

实例访问:向下

  • 通信源:父组件与子组件
  • 数据方向:父组件 <=> 子组件(可任意方向或双向同时)
  • 信号方向:父组件 => 子组件

输入属性的一个优势是低耦合性,父组件无需知晓子组件/指令的类型信息,只需要已经子组件的一个或几个输入项即可。

但是有些时候,当组件/指令间有明确的固定关系,并且我们需要细粒度操作的时候,我们也可以选择提升耦合性来简化通信过程。

ng1 中,我们可以在 Directive Definition Object 中指定 require 属性来获取同宿主或父指令(的控制器)的实例。而在 ng2 中我们还可以获取子组件的实例,并且配置更为简单,只需要借助 @ViewChild()@ViewChildren() 或 @ContentChild()@ContentChildren() 声明属性即可:

@Component({
  template: `
    <child></child>
  `
})
class Parent implements AfterViewInit {
  @ViewChild(Child) child: Child

  ngAfterViewInit(): void {
    const someChildProp = this.child.someProp
    const result = this.child.someMethod(‘abc‘)
  }
}

@Component({
  selector: ‘child‘
})
class Child implements OnInit {
  someProp: number

  someMethod(input: string): string {
    return `${input} operated by child`
  }
}

上面的代码中,我们在父组件中获取到了子组件的实例,并且直接访问子组件的公开属性和方法(TypeScript 不加可访问性修饰符即默认为 public)。之所以需要在 AfterViewInit 这个生命周期后才能操作,是由于父组件的初始化过程在子组件之前,因此在父组件的构造函数或 OnInit 阶段子组件还未实例化,当然也就无从获取。

这样可以较为方便的实现复杂操作,例如同时输入或输出多项数据(如果使用多个输出属性会很 Tricky,因为事件响应相互独立),还能够进行实时反馈(即双向数据传输)。

一个常见的例子是我们基于 NgModel 封装自己的输入控件,其中往往会需要对 NgModel 的 API 进行细粒度操作。对于这样的复杂操作而言,基于数据绑定和事件绑定会让代码过于复杂,工程上几乎不可行。

实例访问:向上

  • 通信源:父组件与子组件/同宿主组件与指令
  • 数据方向:父组件 <=> 子组件/任一组件或指令 <=> 任一组件或指令
  • 信号方向:父组件 <= 子组件/任一组件或指令(使用依赖方) => 任一组件或指令(作为依赖方)

同样的,我们也能够从子组件/指令获取父组件或同宿主组件/指令的实例,具体的方式对于大家来说既熟悉又陌生,那就是依赖注入:

@Component({
  template: `
    <child></child>
    <child></child>
    <child></child>
  `
})
class Parent implements AfterViewInit {
  children: Child[] = []

  register(child: Child) {
    this.children.push(child)
  }
}

@Component({
  selector: ‘child‘
})
class Child implements OnInit {
  constructor(private parent: Parent) {}

  ngOnInit(): void {
    this.parent.register(this)
  }
}

上面的代码中,我们在子组件的构造函数中注入了父组件的实例,如果有需要我们还可以通过 @Self()@SkipSelf() 和 @Host() 来限制该实例的来源,比 ng1 中的 ^ 符号组合显然清晰的多。

由于子组件/指令构造时父组件早已构造完成,因此可以无需等待直接获取到父组件的实例。

这里我们使用了一个子组件自行向父组件登记自身存在的例子,相比于父组件一次性获取所有子组件实例,这样的优势是能够动态增删子组件列表。一个应用实例就是 NgForm 与 NgControl 之间的交互,由于表单可能在使用过程中动态变化,所以无法在表单初始化时一次性获取所有控件实例,而需要支持使用中动态注册与注销控件的功能。

实例访问:服务

  • 通信源:组件与服务
  • 数据方向:组件 <=> 服务
  • 信号方向:组件 <=> 服务

事实上,实例操纵这种方式我们一直都在使用,例如组件对服务的访问:

@Component({
  template: `
    <p>Whatever</p>
  `
})
class SomeComponent implements OnInit {
  constructor(private someService: SomeService) { }

  ngOnInit(): void {
    this.someService.someMethod()
  }
}

@Injectable()
class SomeService {
  someMethod(): void { }
}

上面的代码中,我们使用 @Injectable() 来修饰我们的服务。不过事实上,@Injectable() 并不是指可以被注入到别的内容中,而是指别的内容可以被注入进来,由于我们这里 SomeService 并不依赖于其他内容,故完全可以不使用 @Injectable()。但为了代码一致性,对全体服务都使用 @Injectable() 装饰能够让代码更加清晰。

此外,服务也一样能够配合 Observable 使用,例如 Location 和 ActivatedRoute 就提供了持续的事件流,因此也能够实现服务到组件的信号传递。

输出:事件

  • 通信源:父组件与子组件
  • 数据方向:父组件 <= 子组件(可空)
  • 信号方向:父组件 <= 子组件

与输入属性相对应,每个组件/指令都可以通过 @Output 来指定输出属性,每个输出属性都是 EventEmitter 的一个实例,前者继承自 Reactive Extensions 中的 Subject

@Component({
  template: `
    <child (output)="onOutput($event)"></child>
  `
})
class Parent {
  onOutput(event: number): void {
    console.log(event)
  }
}

@Component({
  selector: ‘child‘
})
class Child implements OnInit {
  @Output() output = new EventEmitter<number>()

  onInit(): void {
    this.output.emit(123)
  }
}

由于这里的 Subject 由 ng 进行管理,我们无需关心 subscribe 和 unsubscribe 的调用,只需要简单应对事件侦听即可。

提供商:单值

  • 通信源:父组件与子组件
  • 数据方向:父组件 => 子组件
  • 信号方向:父组件 => 子组件

归功于 ng2 引入的 Hierarchical Injector 机制,每个组件/指令都可以有独立(并继承)的 Injector。相比于 ng1 中的全局唯一的 Injector 而言,在 ng2 中我们可以对提供商进行细粒度控制。

我们可以使用 @Optional() 的依赖来进行数据传递(或者在模块/根组件中提供默认内容):

@Component({
  template: `
    <child></child>
  `,
  providers: [
    { provide: ‘someToken‘, useValue: 123 }
  ]
})
class Parent { }

@Component({
  selector: ‘child‘
})
class Child implements OnInit {
  constructor(@Inject(‘someToken‘) private someProp: number) { }
}

通过提供商(默认为单值)进行通信的一个特点是静态性,即所需传输的内容一经确定就不可再更改(我们这里使用了 useValue 提供常量,实际上还能通过 useFactory 即时生成内容),并且具有明确的层次性,上层能够对所有下层提供数据,并且中间层能够覆盖上层内容。

一个很常见的例子就是用于制作开关(或其他辅助标识),或者应用策略模式。

提供商:多值

  • 通信源:同宿主组件与指令
  • 数据方向:任一组件/指令 <= 若干组件/指令
  • 信号方向:任一组件/指令 => 若干组件/指令

上面我们已经知道了 @ViewChildren() ,可以一次性获取到全体某个类型的子组件/指令列表。同时也知道了依赖注入可以得到同宿主的组件/指令实例。

但还有一个场景解决不了,就是我们需要得到同宿主的多个同 “类型” 的全体指令。(当然,这里的类型并不是真的 JavaScript 类型,因为一个指令在一个元素上至多只会被应用一次,可以理解为相同标识)

在 ng2 中,有一个黑魔法可以解决这个问题,就是设置了 multi: true 的提供商,这类提供商可以被多次注册,并且不会被覆盖,而是会进行汇总:

@Component({
  template: `
    <p>Whatever</p>
  `
})
class SomeComponent {
  constructor(@Inject(‘magicNumber‘) tokens: number[]) {
    console.log(tokens) // [1, 2]
  }
}

@Directive({
  selector: ‘[propOne]‘,
  providers: [
    { provide: ‘magicNumber‘, useValue: 1, multi: true }
  ]
})
class DirectiveOne { }

@Directive({
  selector: ‘[propTwo]‘,
  providers: [
    { provide: ‘magicNumber‘, useValue: 2, multi: true }
  ]
})
class DirectiveTwo { }

这样,通过某个共同的 Token,每个组件/指令都可以得到其他组件/指令给出的材料,而无需知晓其他组件/指令的具体存在。

一个应用实例是 FormControlName 与 Validator 及 AsyncValidator 之间的交互,所有 Validator 指令都直接应用在 FormControl 所在的元素上,而 FormControl 无需知道每个 Validator 指令的具体形式(无论是内置的还是自定义的),只需要收集每个 Validator 指令所提供的验证函数即可。

当然,这并不是 multi: true 的唯一作用,比如我们还能通过 APP_BOOTSTRAP_LISTENER 来监听应用的启动等等。

速查表

说了这么多,那么我们在应用中应该如何选择这些通信方式呢?这里提供了简单的决策树,以帮助读者快速进行查阅。(仅仅提供参考,并不一定是具体场景下最优选择,实际项目请以自身实际情况为准)

1.是否为组件/指令间通信?
|
|- T
|  |- 2. 是否有位置关系?
|     |
|     |- T
|     |  |- 3. 是否有明确的行为关联(固定搭配)?
|     |     |
|     |     |- T
|     |     |  |- 4. 是否具有固定的上下级关系
|     |     |     |
|     |     |     |- T
|     |     |     |  |- 5. 是否仅需由上至下提供不可变内容?
|     |     |     |     |
|     |     |     |     |- T
|     |     |     |     |  |- (提供商:单值)
|     |     |     |     |
|     |     |     |     |- F
|     |     |     |        |- 6. 子组件/指令是否会动态变化?
|     |     |     |           |
|     |     |     |           |- T
|     |     |     |           |  |- (实例访问:向上)
|     |     |     |           |
|     |     |     |           |- F
|     |     |     |              |- (实例访问:向下)
|     |     |     |- F
|     |     |        |- 7. 是否明确处于同一宿主内?
|     |     |           |
|     |     |           |- T
|     |     |           |  |- 8. 是否有多个组件/指令同时作为数据源?
|     |     |           |     |
|     |     |           |     |- T
|     |     |           |     |  |- 9. 是否仅需提供不可变内容?
|     |     |           |     |     |
|     |     |           |     |     |- T
|     |     |           |     |     |  |- (提供商:多值)
|     |     |           |     |     |
|     |     |           |     |     |- F
|     |     |           |     |        |- (/*借助父组件/指令通信*/)
|     |     |           |     |- F
|     |     |           |        |- 10. 是否仅需提供不可变内容?
|     |     |           |           |
|     |     |           |           |- T
|     |     |           |           |  |- (提供商:单值)
|     |     |           |           |
|     |     |           |           |- F
|     |     |           |              |- (实例访问:向上)
|     |     |           |- F
|     |     |              |- 11. 是否明确为兄弟关系?
|     |     |                 |- T
|     |     |                 |  |- (/*借助父组件/指令通信*/)
|     |     |                 |
|     |     |                 |- F
|     |     |                    |- 那还叫什么固定搭配!
|     |     |- F
|     |        |- 12. 方向是否为由父向子?
|     |           |
|     |           |- T
|     |           |  |- 13. 输入是否影响自身以外的其他子组件内部状态?
|     |           |     |
|     |           |     |- T
|     |           |     |  |- (输入:事件)
|     |           |     |
|     |           |     |- F
|     |           |        |- (输入:数据)
|     |           |- F
|     |              |- 方向是否为由子向父?
|     |                 |
|     |                 |- T
|     |                 |  |- (输出:事件)
|     |                 |
|     |                 |- F
|     |                    |- (/*借助父组件/指令通信*/)
|     |- F
|        |- (/*借助服务通信*/)
|
|- F
   |- 是否为组件与服务间通信
      |- T
      |  |- (实例访问:服务)
      |
      |- F
         |- 并不确定你要做什么~

总结

  1. ng2 应用结构基于组件树;
  2. 组件/指令相互之间,组件/指令与服务之间需要相互通信;
  3. 通信方式有很多种,选择合适的通信方式对应用实现会有很大帮助。
时间: 2024-08-04 02:10:01

Angular2中的通讯方式的相关文章

Angular1组件通讯方式总结

这里需要将Angular1分为Angular1.5之前和Angular1.5两个不同的阶段来讲,两者虽然同属Angular1,但是在开发模式上还是有较大区别的.在Angular1.4及以前,主要是基于HTML的,将所有view划分为不同的HTML片段,通过路由,transclude,include等方式,按照用户行为切换显示不同界面.对于每个template内部来讲,可以是纯HTML,也可以是自定义的directive.directive之间可以有层级关系,也可以没有层级关系.在Angular1

Linux 进程间通讯方式 pipe()函数 (转载)

转自:http://blog.csdn.net/ta893115871/article/details/7478779 Linux 进程间通讯方式有以下几种: 1->管道(pipe)和有名管道(fifo). 2->消息队列 3->共享内存 4->信号量 5->信号(signal) 6->套接字(sicket) 在这里我们看一下第一种====管道(pipe).有名管道(fifo)见其它文章. eg :我们以前学的命令 cat  file | grep  "abc

iOS开发-Socket通讯方式

1.程序之间的通信 两个应用程序之间的通信,我们可以理解为进程之间的通信,而进程之间进行通信的前提是我们能够找到某个进程,因此,我们需要给进程添加唯一的标示,在本地进程通信中我们可以使用PID来标示一个进程,但PID只在本地唯一,网络中的多个计算机之间的进程标示并不能保证唯一性,冲突的几率很大,这时候我们需要另辟蹊径,TCP/IP协议族已为我们解决了这个问题,IP层的ip地址可以标示主机,而TCP层协议和端口号可以标示某个主机的某个进程,于是我们采取"ip地址+协议+端口号"作为唯一标

Angular2中的路由(简单总结)

Angular2中建立路由的4个步骤: 1.路由配置:最好新建一个app.toutes.ts文件(能不能用ng命令新建有待调查) Angular2中路由要解决的是URL与页面的对应关系(比如URL是http://localhost:4200/all-people,那么页面显示的就应该是allPeople画面,URL是http://localhost:4200/first-come,页面显示的就应该是firstCome画面). 在Angular2中页面是由组件组成的(Angular2中的根模块对应

进程和线程的定义及区别、线程同步、进程通讯方式总结

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 一. 进程的概念 进程是在多道程序系统出现以后,为了描述系统内部各作业的活动规律而引进的概念. 由 于多道程序系统所带来的复杂环境,程序本身有了并行性[为了充分利用资源,在主存中同时存放多道作业运行,所以各作业之间是并行的].制约性[各程序由于 同时存在于主存中,因此他们之间会存在着相互依赖.相互制约的关系.一个是通过中间媒介--资源发生的间接制约关系,一个是各并行程序间需要相互协同而引 起

Matlab中TCP通讯-实现外部程序提供优化目标函数解

版权声明:若无来源注明,Techie亮博客文章均为原创. 转载请以链接形式标明本文标题和地址: 本文标题:Matlab中TCP通讯-实现外部程序提供优化目标函数解     本文地址:http://techieliang.com/2017/12/551/ 文章目录 1. 介绍 2. TCP使用方法  2.1. 创建tcp  2.2. 开启tcp  2.3. 关闭tcp  2.4. 收发 3. 其他  3.1. matlab发送回车,换行符的方法  3.2. matlab字符串连接  3.3. 接收

线程与进程之间的通讯方式

1.认识线程和进程: 1.1什么是线程:线程是系统执行任务调度的最小单位,一个进程可以只包含一个线程此时线程也可以理解为进程,当然也可以拥有多个线程,线程之间可以实现资源共享以及通讯什么是进程:系统资源分配的最小单位线程和进程区别:实际上,进程不是同时运行的,对于一个 CPU 而言,某个时间段只能运行一个程序,也就是只能执行一个进程.操作系统会为每个进程分配一段有限的 CPU 使用时间,CPU 在这段时间内执行某个进程,然后会在下一段时间切换到另一个进程中去执行.为什么会分进程和线程,进程一次切

Mac OS X中,有三种方式来实现启动项的配置

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; text-align: center; font: 22.0px "Times New Roman"; color: #000000 } p.p2 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px "Times New Roman"; color: #000000 } p.p3 { margin: 0.0px 0.0px 0.0px 0.0p

iOS中的桥接方式

iOS中的桥接方式 C ->OC (__bridge tupe)expression  : 将CoreFoundation框架的对象所有权交给Foundation框架来使用,但是Foundation框架中的对象并不能管理该对象内存 (__bridge_transfer Objective-C type)expression : 将CoreFoundation框架的对象所有权交给Foundation来管理,如果Foundation中对象销毁,那么我们之前的对象(CoreFoundation)会一起