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})`); }
}
