События жизненного цикла Anuglar2 как rxjs Наблюдаемые

Есть ли способ построить angular2 события жизненного цикла, такие как OnDestroy как rxjs Observable?

Я хотел бы подписаться на наблюдаемое подобное:

ngOnInit() {
  MyService.myCustomFunction()
    .takeUntil(NgOnDestroy)  //NgOnDestroy would be the lifecycle observable
    .subscribe(() => {
      //any code
    });
 }

Что кажется интуитивным и лучше читать, чем:

private customObservable: Observable;

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

ngOnInit() {
  this.customObservable = MyService.myCustomFunction()
    .subscribe(() => {
      //any code
    });
 }

Ответ 1

Существует не встроенный способ, но вы можете настроить декоратор или базовый класс, чтобы сделать это, если вы не хотите ждать.

Базовый класс

Это решение работает с AOT. Однако в более старых версиях Angular была ошибка, при которой события жизненного цикла на базовых классах не регистрировались при использовании AOT. Похоже, что это работает в 4.4.x+. Вы можете получить дополнительную информацию здесь, чтобы узнать, будет ли затронута ваша версия: https://github.com/angular/angular/issues/12922

Пример

import { SimpleChanges, OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/take';

const onChangesKey = Symbol('onChanges');
const onInitKey = Symbol('onInit');
const doCheckKey = Symbol('doCheck');
const afterContentInitKey = Symbol('afterContentInit');
const afterContentCheckedKey = Symbol('afterContentChecked');
const afterViewInitKey = Symbol('afterViewInit');
const afterViewCheckedKey = Symbol('afterViewChecked');
const onDestroyKey = Symbol('onDestroy');

export abstract class LifeCycleComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
    // all observables will complete on component destruction
    protected get onChanges(): Observable<SimpleChanges> { return this.getObservable(onChangesKey).takeUntil(this.onDestroy); }
    protected get onInit(): Observable<void> { return this.getObservable(onInitKey).takeUntil(this.onDestroy).take(1); }
    protected get doCheck(): Observable<void> { return this.getObservable(doCheckKey).takeUntil(this.onDestroy); }
    protected get afterContentInit(): Observable<void> { return this.getObservable(afterContentInitKey).takeUntil(this.onDestroy).take(1); }
    protected get afterContentChecked(): Observable<void> { return this.getObservable(afterContentCheckedKey).takeUntil(this.onDestroy); }
    protected get afterViewInit(): Observable<void> { return this.getObservable(afterViewInitKey).takeUntil(this.onDestroy).take(1); }
    protected get afterViewChecked(): Observable<void> { return this.getObservable(afterViewCheckedKey).takeUntil(this.onDestroy); }
    protected get onDestroy(): Observable<void> { return this.getObservable(onDestroyKey).take(1); }

    ngOnChanges(changes: SimpleChanges): void { this.emit(onChangesKey, changes); };
    ngOnInit(): void { this.emit(onInitKey); };
    ngDoCheck(): void { this.emit(doCheckKey); };
    ngAfterContentInit(): void { this.emit(afterContentInitKey); };
    ngAfterContentChecked(): void { this.emit(afterContentCheckedKey); };
    ngAfterViewInit(): void { this.emit(afterViewInitKey); };
    ngAfterViewChecked(): void { this.emit(afterViewCheckedKey); };
    ngOnDestroy(): void { this.emit(onDestroyKey); };

    private getObservable(key: symbol): Observable<any> {
        return (this[key] || (this[key] = new Subject<any>())).asObservable();
    }

    private emit(key: symbol, value?: any): void {
        const subject = this[key];
        if (!subject) return;
        subject.next(value);
    }
}

Использование

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

import { LifeCycleComponent } from './life-cycle.component';
import { MyService } from './my.service'

@Component({
  template: ''
})
export class TestBaseComponent extends LifeCycleComponent implements OnInit {
  constructor(private myService: MyService) {
    super();
  }

  ngOnInit() {
    super.ngOnInit();
    this.myService.takeUntil(this.onDestroy).subscribe(() => {});
  }
}

Поскольку вы наследуете, убедитесь, что если вы склонны внедрять один из интерфейсов жизненного цикла, который вы также вызываете метод базового класса (например, ngOnInit() { super.ngOnInit(); }).

декоратор

Это решение не работает с AOT. Лично мне нравится этот подход лучше, но он не работает с AOT - это своего рода прерыватель транзакций для некоторых проектов.

Пример

/**
 * Creates an observable property on an object that will
 * emit when the corresponding life-cycle event occurs.
 * The main rules are:
 * 1. Don't name the property the same as the angular interface method.
 * 2. If a class inherits from another component where the parent uses this decorator
 *    and the child implements the corresponding interface then it needs to call the parent method.
 * @param {string} lifeCycleMethodName name of the function that angular calls for the life-cycle event
 * @param {object} target class that contains the decorated property
 * @param {string} propertyKey name of the decorated property
 */
function applyLifeCycleObservable(
    lifeCycleMethodName: string,
    target: object,
    propertyKey: string
): void {
    // Save a reference to the original life-cycle callback so that we can call it if it exists.
    const originalLifeCycleMethod = target.constructor.prototype[lifeCycleMethodName];

    // Use a symbol to make the observable for the instance unobtrusive.
    const instanceSubjectKey = Symbol(propertyKey);
    Object.defineProperty(target, propertyKey, {
        get: function() {
            // Get the observable for this instance or create it.
            return (this[instanceSubjectKey] || (this[instanceSubjectKey] = new Subject<any>())).asObservable();
        }
    });

    // Add or override the life-cycle callback.
    target.constructor.prototype[lifeCycleMethodName] = function() {
        // If it hasn't been created then there no subscribers so there is no need to emit
        if (this[instanceSubjectKey]) {
            // Emit the life-cycle event.
            // We pass the first parameter because onChanges has a SimpleChanges parameter.
            this[instanceSubjectKey].next.call(this[instanceSubjectKey], arguments[0]);
        }

        // If the object already had a life-cycle callback then invoke it.
        if (originalLifeCycleMethod && typeof originalLifeCycleMethod === 'function') {
            originalLifeCycleMethod.apply(this, arguments);
        }
    };
}

// Property Decorators
export function OnChangesObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngOnChanges', target, propertyKey);
}
export function OnInitObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngOnInit', target, propertyKey);
}
export function DoCheckObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngDoCheck', target, propertyKey);
}
export function AfterContentInitObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterContentInit', target, propertyKey);
}
export function AfterContentCheckedObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterContentChecked', target, propertyKey);
}
export function AfterViewInitObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterViewInit', target, propertyKey);
}
export function AfterViewCheckedObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngAfterViewChecked', target, propertyKey);
}
export function OnDestroyObservable(target: any, propertyKey: string) {
    applyLifeCycleObservable('ngOnDestroy', target, propertyKey);
}

Использование

import { Component, OnInit, Input, SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import {
    OnChangesObservable,
    OnInitObservable,
    DoCheckObservable,
    AfterContentInitObservable,
    AfterContentCheckedObservable,
    AfterViewInitObservable,
    AfterViewCheckedObservable,
    OnDestroyObservable
 } from './life-cycle.decorator';
import { MyService } from './my.service'

@Component({
    template: ''
})
export class TestDecoratorComponent implements OnInit {

    @OnChangesObservable
    onChanges: Observable<SimpleChanges>;
    @OnInitObservable
    onInit: Observable<void>;
    @DoCheckObservable
    doCheck: Observable<void>;
    @AfterContentInitObservable
    afterContentInit: Observable<void>;
    @AfterContentCheckedObservable
    afterContentChecked: Observable<void>;
    @AfterViewInitObservable
    afterViewInit: Observable<void>;
    @AfterViewCheckedObservable
    afterViewChecked: Observable<void>;
    @OnDestroyObservable
    onDestroy: Observable<void>;

    @Input()
    input: string;

    constructor(private myService: MyService) {
    }

    ngOnInit() {
        this.myService.takeUntil(this.onDestroy).subscribe(() => {});
        this.onChanges
            .map(x => x.input)
            .filter(x => x != null)
            .takeUntil(this.onDestroy)
            .subscribe((change: SimpleChange) => {
            });
    }
}

Есть несколько правил относительно этого решения, которое, я думаю, разумно соблюдать:

  • Назовите свою собственность чем-либо, кроме имени метода angular, чтобы вызывать уведомление об объекте события жизненного цикла (например, не называть свойство ngOnInit). Это связано с тем, что декоратор создаст свойство как геттер и должен будет создать этот метод в классе для перехвата события жизненного цикла. Если вы проигнорируете это, вы получите ошибку времени выполнения.
  • Если вы наследуете класс, который использует декоратор свойств жизненного цикла, а дочерний класс реализует интерфейс angular для соответствующего события, тогда дочерний класс должен вызывать метод родительского класса (например, ngOnInit() { super.ngOnInit(); }). Если вы проигнорируете это, тогда ваше наблюдаемое свойство не будет излучаться, потому что метод родительского класса затенен.
  • У вас может возникнуть соблазн сделать что-то вроде этого вместо реализации интерфейса angular: this.onInit.subscribe(() => this.ngOnInit()). Не. Это не волшебство. angular просто проверяет наличие функции. Поэтому назовите метод, который вы вызываете, чтобы подписаться на что-то другое, кроме интерфейса angular. Если вы проигнорируете это, вы создадите бесконечный цикл.

Вы все равно можете реализовать стандартные angular интерфейсы для событий жизненного цикла, если хотите. Декоратор будет перезаписывать его, но он будет выделяться на наблюдаемом, а затем ссылаться на вашу оригинальную реализацию. В качестве альтернативы вы можете просто подписаться на соответствующие наблюдаемые.

-

Одним из преимуществ является то, что он в основном позволяет наблюдать ваши свойства @Input, поскольку ngOnChanges теперь можно наблюдать. Вы можете настроить фильтр с помощью карты, чтобы создать поток для значения свойства (например, this.onChanges.map(x => x.myInput).filter(x => x != null).subscribe(x => { ... });).

В этом редакторе для этого примера была введена большая часть кода, поэтому возможны синтаксические ошибки. Вот пример запуска, который я настраиваю, когда играю с ним. Откройте консоль, чтобы увидеть огонь событий.

https://codepen.io/bygrace1986/project/editor/AogqjM