Angular 2 Components Communicate

semlinker發表於2019-02-16

本文介紹的內容是元件通訊的常用方式:@Input、@Output、@ViewChild、模板變數、MessageService、Broadcaster (Angular 1.x $rootScope 中 $on、$broadcast ) 和 Pub – Sub 模式、RxJS Subject 存在的問題。

輸入屬性 (父元件 -> 子元件)

counter.component.ts

import { Component, Input } from `@angular/core`;

@Component({
    selector: `exe-counter`,
    template: `
      <p>當前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    `
})
export class CounterComponent {
    @Input() count: number = 0;

    increment() {
        this.count++;
    }

    decrement() {
        this.count--;
    }
}

app.component.ts

import { Component } from `@angular/core`;

@Component({
  selector: `exe-app`,
  template: `
   <exe-counter [count]="initialCount"></exe-counter>
  `
})
export class AppComponent {
  initialCount: number = 5;
}

輸出屬性 (子元件 -> 父元件)

counter.component.ts

import { Component, Input, Output, EventEmitter } from `@angular/core`;

@Component({
    selector: `exe-counter`,
    template: `
      <p>當前值: {{ count }}</p>
      <button (click)="increment()"> + </button>
      <button (click)="decrement()"> - </button>
    `
})
export class CounterComponent {
    @Input() count: number = 0;

    @Output() change: EventEmitter<number> = new EventEmitter<number>();

    increment() {
        this.count++;
        this.change.emit(this.count);
    }

    decrement() {
        this.count--;
        this.change.emit(this.count);
    }
}

app.component.ts

import { Component } from `@angular/core`;

@Component({
  selector: `exe-app`,
  template: `
   <p>{{changeMsg}}</p> 
   <exe-counter [count]="initialCount" 
    (change)="countChange($event)"></exe-counter>
  `
})
export class AppComponent {
  initialCount: number = 5;

  changeMsg: string;

  countChange(event: number) {
    this.changeMsg = `子元件change事件已觸發,當前值是: ${event}`;
  }
}

模板變數

child.component.ts

import {Component} from `@angular/core`;

@Component({
  selector: `child-component`,
  template: `I`m {{ name }}`
})

export class ChildComponent {
  public name: string;
}

parent.component.ts

import {Component, OnInit} from `@angular/core`;
import {ChildComponent} from `./child-component.ts`;

@Component({
  selector: `parent-component`,
  template: `
    <child-component #child></child-component>
    <button (click)="child.name = childName">設定子元件名稱</button>
  `
})

export class ParentComponent implements OnInit {
  
  private childName: string;
  
  constructor() { }

  ngOnInit() { 
    this.childName = `child-component`;
  }
}

@ViewChild 裝飾器

child.component.ts

import { Component, OnInit } from `@angular/core`;

@Component({
    selector: `exe-child`,
    template: `
      <p>Child Component</p>  
    `
})
export class ChildComponent {
    name: string = ``;
}

app.component.ts

import { Component, ViewChild, AfterViewInit } from `@angular/core`;
import { ChildComponent } from `./child.component`;

@Component({
  selector: `my-app`,
  template: `
    <h4>Welcome to Angular World</h4>
    <exe-child></exe-child>
  `,
})
export class AppComponent {

  @ViewChild(ChildComponent)
  childCmp: ChildComponent;

  ngAfterViewInit() {
    this.childCmp.name = `child-component`;
  }
}

使用 MessageService – 基於 RxJS Subject

message.service.ts

import { Injectable } from `@angular/core`;
import {Observable} from `rxjs/Observable`;
import { Subject } from `rxjs/Subject`;

@Injectable()
export class MessageService {
    private subject = new Subject<any>();

    sendMessage(message: string) {
        this.subject.next({ text: message });
    }

    clearMessage() {
        this.subject.next();
    }

    getMessage(): Observable<any> {
        return this.subject.asObservable();
    }
}

home.component.ts

import { Component } from `@angular/core`;
import { MessageService } from `./message.service`;

@Component({
    selector: `exe-home`,
    template: `
    <div>
        <h1>Home</h1>
        <button (click)="sendMessage()">Send Message</button>
        <button (click)="clearMessage()">Clear Message</button>
    </div>`
})

export class HomeComponent {
    constructor(private messageService: MessageService) {}
    
    sendMessage(): void {
        this.messageService.sendMessage(`Message from Home Component to App Component!`);
    }

    clearMessage(): void {
        this.messageService.clearMessage();
    }
}

app.component.ts

import { Component, OnDestroy } from `@angular/core`;
import { Subscription } from `rxjs/Subscription`;
import { MessageService } from `./message.service`;

@Component({
    selector: `my-app`,
    template: `
    <div>
       <div *ngIf="message">{{message.text}}</div>
       <exe-home></exe-home>
    </div>
    `
})

export class AppComponent implements OnDestroy {
    message: any;
    subscription: Subscription;

    constructor(private messageService: MessageService) {
        this.subscription = this.messageService
                                  .getMessage().subscribe( message => { 
                                      this.message = message; 
                                 });
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}

使用 Broadcaster – 基於 RxJS Subject

實現 Angular 1.x 中的 $rootScope 物件中 $on$broadcast 的功能。

broadcaster.ts

import { Injectable } from `@angular/core`;
import {Subject} from `rxjs/Subject`;
import {Observable} from `rxjs/Observable`;
import `rxjs/add/operator/filter`;
import `rxjs/add/operator/map`;

interface BroadcastEvent {
  key: any;
  data?: any;
}

@Injectable()
export class Broadcaster {
  private _eventBus: Subject<BroadcastEvent>;

  constructor() {
    this._eventBus = new Subject<BroadcastEvent>();
  }

  broadcast(key: any, data?: any) {
    this._eventBus.next({key, data});
  }

  on<T>(key: any): Observable<T> {
    return this._eventBus.asObservable()
      .filter(event => event.key === key)
      .map(event => <T>event.data);
  }
}

child.component.ts

import { Component } from `@angular/core`;

@Component({
    selector: `child`
})
export class ChildComponent {
  constructor(private broadcaster: Broadcaster) {}
  
  registerStringBroadcast() {
    this.broadcaster.on<string>(`MyEvent`)
      .subscribe(message => {
        ...
      });
  }

  emitStringBroadcast() {
    this.broadcaster.broadcast(`MyEvent`, `some message`);
  }
}

本文主要是介紹元件通訊的思路,提供的都是相對簡單的示例。如果想深入瞭解,請參考 – angular.cn – component-communication

我有話說

1.在實際開發中,我們也經常使用 Pub (釋出) – Sub (訂閱模式) 來實現模組之間的訊息通訊。接下來我們看一下 Pub – Sub 的核心點:

  • 至少包含 subscribe() 和 publish() 兩個方法,subscribe() 用於實現訊息訂閱,publish() 方法用於釋出訊息。

  • 支援訂閱不同的訊息型別,且對於任何一種訊息型別都可以新增多個觀察者。內部實現一般使用 key-value 結構進行訊息型別和觀察者列表的儲存。

  • 訂閱觀察者後,最好返回一個物件或函式物件,用於取消訂閱。

具體示例如下(詳細資訊,請參考 – Pub/Sub JavaScript Object):

var events = (function(){
  var topics = {};
  var hOP = topics.hasOwnProperty;

  return {
    subscribe: function(topic, listener) {
      // 如果topic型別不存在,則建立
      if(!hOP.call(topics, topic)) topics[topic] = [];
      
      // 新增listener
      var index = topics[topic].push(listener) -1;

      // 返回物件用於移除listener
      return {
        remove: function() {
          delete topics[topic][index];
        }
      };
    },
    publish: function(topic, info) {
      if(!hOP.call(topics, topic)) return;

      topics[topic].forEach(function(item) {
              item(info != undefined ? info : {});
      });
    }
  };
})();

使用示例:

var subscription = events.subscribe(`/page/load`, function(obj) {
    // 事件處理
});

events.publish(`/page/load`, {
    url: `/some/url/path` 
});

2.RxJS Subject 在使用中存在一個問題,就是如果某個 observer (觀察者) 在執行的時候出現異常,卻沒有進行異常處理,那麼就會影響到其它的觀察者。解決該問題,最簡單的方式就是為所有的觀察者新增異常處理。具體問題如下:

const source = Rx.Observable.interval(1000);
const subject = new Rx.Subject();

const example = subject.map(x => {
    if (x === 1) {
        throw new Error(`oops`);
    }
    return x;
});
subject.subscribe(x => console.log(`A`, x));
example.subscribe(x => console.log(`B`, x));
subject.subscribe(x => console.log(`C`, x));

source.subscribe(subject);

以上程式碼執行後,控制檯的輸出結果:

A 0
B 0
C 0
A 1
Rx.min.js:74 Uncaught Error: oops

解決方案:

const source = Rx.Observable.interval(1000);
const subject = new Rx.Subject();

const example = subject.map(x => {
    if (x === 1) {
        throw new Error(`oops`);
    }
    return x;
});

subject.subscribe(
    x => console.log(`A`, x),
    error => console.log(`A Error:` + error)
);
    
example.subscribe(
    x => console.log(`B`, x),
    error => console.log(`B Error:` + error)
);

subject.subscribe(
    x => console.log(`C`, x),
    error => console.log(`C Error:` + error)
);

source.subscribe(subject);

關於 RxJS Subject 的詳細資訊,請檢視 – RxJS Subject

相關文章