網站搜尋

typescript - 如何從 Angular2 組件模板子元素事件創建 RxJS 可觀察流


我正在使用 Angular 2.0.0-rc.4 搭配 RxJS 5.0.0-beta.6。

我正在試驗從事件中創建可觀察流的不同方法,但對選項感到不知所措,並想徵求意見。我理解沒有一種通用的解決方案,各有各的適用方法。可能還有其他我不知道或未考慮過的技術。

Angular 2 組件互動手冊提供了多種父組件與子組件事件互動的方法。然而,只有父組件和子組件通過服務進行通信的例子使用了可觀察對象,對大多數情況來說似乎有些過度設計。

情境是模板中的一個元素發出大量事件,而我想定期知道最新的值是什麼。

我使用 Observable 的 sampleTime 方法,週期為 1000 毫秒,以監控 <p> HTML 元素上的滑鼠位置。

1) 這種技術使用注入到元件建構函數中的 ElementRef,以訪問 nativeElement 屬性,並通過標籤名稱查詢子元素。

@Component({
  selector: 'watch-child-events',
  template: `
    <p>Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];

  constructor(private el:ElementRef) {}

  ngOnInit() {
    let p = this.el.nativeElement.getElementsByTagName('p')[0];
    Observable
      .fromEvent(p, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

共識意見似乎對這種技術不太贊同,因為 Angular2 已經提供了對 DOM 足夠的抽象,所以你很少甚至幾乎不需要直接與它互動。但 Observable 的 fromEvent 工廠方法使它相當具有吸引力,而且這也是最先出現在我腦海中的技術。

2) 這個技術使用了一個 EventEmitter,它是一個 Observable。

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  emitter:EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();

  ngOnInit() {
    this.emitter
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.emitter.emit(e);
  }
}

這個解決方案避免了查詢 DOM,但事件發射器用於從子組件向父組件傳遞信息,而且這個事件不是用於輸出。

我在這裡讀到,不應假設事件發射器在最終版本中會是可觀察的,因此這可能不是一個可靠依賴的穩定功能。

3) 這個技術使用一個可被觀察的 Subject。

@Component({
  selector: 'watch-child-events',
  template: `
    <p (mousemove)="handle($event)">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

在我看來,這個解決方案在不增加太多複雜性的情況下滿足了所有需求。我可以使用 ReplaySubject 在訂閱時接收已發佈值的完整歷史,或者只接收最近的一個值(如果存在),方法是使用 subject=new ReplaySubject<MouseEvent>(1); 代替。

4) 這個技術使用範本參考與 @ViewChild 裝飾器一起使用。

@Component({
  selector: 'watch-child-events',
  template: `
    <p #p">Move over me!</p>
    <div *ngFor="let message of messages">{{message}}</div>
  `
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild('p') p:ElementRef;

  ngAfterViewInit() {
    Observable
      .fromEvent(this.p.nativeElement, 'mousemove')
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

雖然它可行,但對我來說有點怪。模板引用主要是用於模板內的組件互動。它還通過 nativeElement 觸及 DOM,使用字符串來引用事件名稱和模板引用,並使用 AfterViewInit 生命周期鉤子。

5) 我將範例擴展為使用一個自訂元件,該元件管理一個 Subject 並定期發出事件。

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();
  subject = new Subject<MouseEvent>();

  constructor() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.event.emit(e);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

家長像這樣使用它:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];

  handle(e:MouseEvent) {
    this.messages.push(`${e.type} (${e.x}, ${e.y})`);
  }
}

我喜歡這種技術;自訂元件封裝了所需的行為,並讓父元件容易使用,但它只能向上傳遞訊息,無法通知兄弟元件。

6) 將此與這種技術對比,這種技術只是將事件從子級轉發到父級。

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  @Output() event = new EventEmitter<MouseEvent>();

  handle(e:MouseEvent) {
    this.event.emit(e);
  }
}

並且在父組件中使用 @ViewChild 裝飾器連接,如下所示:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements AfterViewInit {
  messages:string[] = [];
  @ViewChild(ChildEventProducerComponent) child:ChildEventProducerComponent;

  ngAfterViewInit() {
    Observable
      .from(this.child.event)
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }
}

7) 或像這樣:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="handle($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent implements OnInit {
  messages:string[] = [];
  subject = new Subject<MouseEvent>();

  ngOnInit() {
    this.subject
      .sampleTime(1000)
      .subscribe((e:MouseEvent) => {
        this.messages.push(`${e.type} (${e.x}, ${e.y})`);
      });
  }

  handle(e:MouseEvent) {
    this.subject.next(e);
  }
}

使用一個可觀察的 Subject,這與之前的技術相同。

8) 最後,如果你需要在元件樹中廣播通知,共用服務似乎是可行的方法。

@Injectable()
export class LocationService {
  private source = new ReplaySubject<{x:number;y:number;}>(1);

  stream:Observable<{x:number;y:number;}> = this.source
    .asObservable()
    .sampleTime(1000);

  moveTo(location:{x:number;y:number;}) {
    this.source.next(location);
  }
}

這個行為被封裝在服務中。子元件中所需的只是將 LocationService 注入到建構函數中,並在事件處理器中呼叫 moveTo。

@Component({
  selector: 'child-event-producer',
  template: `
    <p (mousemove)="handle($event)">
      <ng-content></ng-content>
    </p>
  `
})
export class ChildEventProducerComponent {
  constructor(private svc:LocationService) {}

  handle(e:MouseEvent) {
    this.svc.moveTo({x: e.x, y: e.y});
  }
}

在你需要進行廣播的組件樹層級注入服務。

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer>
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent],
  providers: [LocationService]
})
export class WatchChildEventsComponent implements OnInit, OnDestroy {
  messages:string[] = [];
  subscription:Subscription;

  constructor(private svc:LocationService) {}

  ngOnInit() {
    this.subscription = this.svc.stream
      .subscribe((e:{x:number;y:number;}) => {
        this.messages.push(`(${e.x}, ${e.y})`);
      });
  }

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

在完成後不要忘記取消訂閱。這個解決方案提供了很大的靈活性,但以增加一些複雜性為代價。

總結來說,如果不需要跨組件通信,我會使用組件內部的 Subject (3)。如果需要向組件樹向上通信,我會在子組件中封裝一個 Subject 並在組件內應用流操作符 (5)。否則,如果需要最大靈活性,我會使用服務來封裝一個流 (8)。

答案#1

在方法 6 中,您可以使用事件綁定(向下滾動到「使用 EventEmitter 的自定義事件」)代替 @ViewChild 和 ngAfterViewInit,這樣可以簡化很多:

@Component({
  selector: 'watch-child-events',
  template: `
    <child-event-producer (event)="onEvent($event)">
      Move over me!
    </child-event-producer>
    <div *ngFor="let message of messages">{{message}}</div>
  `,
  directives: [ChildEventProducerComponent]
})
export class WatchChildEventsComponent {
  messages:string[] = [];
  onEvent(e) { this.messages.push(`${e.type} (${e.x}, ${e.y})`); }
}