Angular实践之将Input与Lifecycle转换成流示例详解
Lian Shenghua 人气:0将 Input 和生命周期函数转换成流
在 Angular 中一直有一个期待,就是希望能够将 Input 和生命周期函数转换成流,实际上我们可以通过比较简单的方式实现,比如说:
class NameComponent { @Input() name: string; }
我们要实现一个 input 为 name, output 为 hello name 的简单 component。如果将 input 看成是一个流的话,就会比较容易实现。常见的流转换方式是通过 set 方法实现:
class NameComponent { private name$ = new Subject(1); private _name: string; @Input() set name(val: string) { this.name$.next(val); this._name = val; } get name() { return this._name; } @Output() helloName = this.name$.pipe( map(name => `hello ${name}`), ); }
这样写是可以,不过你也看出来了,有一个问题,就是麻烦。
对于生命周期函数,我们也有类似的需求。比如说,我们经常需要在 Angular 销毁的时候,unsubscribe 所有的 subscription。
class NameComponent implements OnDestroy { private destory$ = new Subject<void>(); ngOnDestroy(): void { destory$.next(); destory$.complete(); } }
如果需要使用其他的生命周期函数的话,每个函数都需要这样手动调用一次。
思路
如果回到 input 的问题的话,我们知道,获取 input 的变化,除了 set 方法,还有 ngOnChanges 函数。我们很容易想到一个思路:
- 将 ngOnChanges 转换成一个 stream, onChanges$
- 通过 onChanges$ map 成 input stream
private onChanges$ = new Subject<SimpleChanges>(); @Input() name: string; name$ = this.onChanges$.pipe( switchMap(simpleChanges => { if ('name' in simpleChanges) { return of(simpleChanges?.name?.currentValue); } return EMPTY; }), ) ngOnChanges(simpleChanges: SimpleChanges) { this.onChanges$.next(simpleChanges); }
当然,ngOnChanges 只会在 input 变化的时候触发,所以我们还需要加上 init 以后的初始值。(当然,我们也要将 afterViewInit 转换成 stream)
name$ = afterViewInit$.pipe( take(1), map(() => this.name), switchMap(value => this.onChanges$.pipe( startWith(value), if ('name' in simpleChanges) { return of(simpleChanges?.name?.currentValue); } return EMPTY; )), )
抽离成一个方法
很明显,如果 input 比较多的话,这样写就比较冗余了。很容易想到我们可以把它抽离成一个方法。
export function getMappedPropsChangesWithLifeCycle<T, P extends (keyof T & string)>( target: T, propName: P, onChanges$: Observable<SimpleChanges>, afterViewInit$: Observable<void>) { if (!onChanges$) { return EMPTY; } if (!afterViewInit$) { return EMPTY; } return afterViewInit$.pipe( take(1), map(() => target?.[propName]), switchMap(value => target.onChanges$.pipe( startWith(value), if (propName in simpleChanges) { return of(simpleChanges?.[propName]?.currentValue); } return EMPTY; )) ) }
看到这里,你可能很容易就想到了,我们可以把这个方法变成一个 decorator,这样看起来就简洁多了。比如我们定义一个叫做 InputMapper 的装饰器:
export function InputMapper(inputName: string) { return function (target: object, propertyKey: string) { const instancePropertyKey = Symbol(propertyKey); Object.defineProperty(target, propertyKey, { get: function () { if (!this[instancePropertyKey]) { this[instancePropertyKey] = getMappedPropsChangesWithLifeCycle(this, inputName, this['onChanges$']!, this['afterViewInit$']!); } return this[instancePropertyKey]; } }); }; }
值得注意的是,因为 target 会是 component instance 的 proto,会被所有的 instance 共享,所以我们在定义变量的时候,可以通过 defineProperty 中的 get 函数将变量定义到 this 上。这样 component instance 在调用的时候就可以成功将内容 apply 到 instance 而费 component class 的 prototype 上。
当然,使用的时候就会方便很多了:
class NameComponent { private onChanges$ = new Subject<SimpleChanges>(); private afterViewInit$ = new Subject<void>(); @Input() name: string; @InputMapper('name') name$!: Observable<string>; ngOnChanges() { ... } ngAfterViewInit() { ... } }
当然,因为对于生命周期函数,也是重复性工作,我们很容易想到,是否能够也能通过装饰器实现。
重写生命周期函数
我们只需要重写生命周期函数就可以巧妙的实现了:
- 用类似的方法,在 this 定义 subject
- 保存原有的函数
- 通过 prototype 重写原来的函数,调用 subject.next。
- 可以参考这里的实现: Angular2+ Observable Life-cycle Events (induro.io)
export function LifeCycleStream(lifeCycleMethodName: LifeCycleMethodName) { return (target: object, propertyKey: string) => { const originalLifeCycleMethod = target.constructor.prototype[lifeCycleMethodName]; const instanceSubjectKey = Symbol(propertyKey); Object.defineProperty(target, propertyKey, { get: function () { if (!this[instanceSubjectKey]) { this[instanceSubjectKey] = new ReplaySubject(1); } return this[instanceSubjectKey].asObservable(); } }); target.constructor.prototype[lifeCycleMethodName] = function () { if (this[instanceSubjectKey]) { this[instanceSubjectKey].next.call(this[instanceSubjectKey], arguments[0]); } if (originalLifeCycleMethod && typeof originalLifeCycleMethod === 'function') { originalLifeCycleMethod.apply(this, arguments); } }; } }
那么我们可以将之前工作简化为:
class NameComponent { @LifeCycleStream('ngOnChanges') onChanges$: Observable<SimpleChanges>; @LifeCycleStream('ngAfterViewInit') ngAfterViewInit$: Observable<void>; @Input() name: string; @InputMapper('name') name$!: Observable<string>; ... ... }
然后,因为我们已经实现了 InputMapper,那么很容易想到,有没有可能把 onChanges$
和 afterViewInit$
放进 InputMapper,这样我们就可以减少重复的调用了。我们可以把 LifeCycleStream 中的主体逻辑抽离成一个方法:applyLifeCycleObservable,然后在 InputMapper 调用就可以了:
if (!('afterViewInit$' in target)) { applyLifeCycleObservable('ngAfterViewInit', target, 'afterViewInit$'); } if (!('onChanges$' in target)) { applyLifeCycleObservable('ngOnChanges', target, 'onChanges$'); }
当然,我们在调用前需要检查这个 stream 是否已经存在。注意,这里不要直接调用 target['ngAfterViewInit'], 因为我们之前写了 get 函数。可以思考下为什么。(防止将 ngAfterViewInit apply 到 target 上去)
最后,我们来看一下最终的代码:
class NameComponent { @Input() name: string; @InputMapper('name') name$!: Observable<string>; }
这样,既没有破坏已有的 angular input,又能够很快的实现,input to stream 的转换,还是比较方便的。
加载全部内容