[zh]
What
如题,实现一个将 Angular 组件 Input 自动转化为 Observable 的自定义拦截器:
@Component({})
export class DemoComponent {
@ObservableInput()
@Input('name')
name$$: Observable<string>;
}
通过上面的 ObservableInput 装饰器,我们将父组件传递的 Input name 自动转化成了一个 Observable 对象。
Why
Angular 组件中我们使用 @Input 获取父组件传递的上下文数据,类似 React/Vue 中 props 的概念。通常我们为了支持 Input 动态变化并做出一些相关操作的情况,会将 @Input 定义为 setter 的方式,同时我们为了取到最新的 Input 值又需要定义一个内部私有变量和一个对应的 getter:
@Component({})
export class DemoComponent {
private _name: string;
@Input()
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
// do something
}
}
很明显,如果项目里的组件的 Input 越来越多且我们都需要支持动态 Input 的话可能会有很多这样的模板代码,且类似 _name 这样的中间变量放在代码里既显得丑陋又影响代码阅读体验,而实际上 Angular 社区对 ObservableInput 的需求已经由来已久:Proposal: Input as Observable,但官方一直未提供相应的实现。
How
目前社区里类似的 ObservableInput 实现也都是通过自定义 getter/settter 劫持的方案来完成数据的转换,但是依然存在一些问题:
- 转化成
Observable 对象后无法直接获取原来 Input 的值了
- 无法给原始
Input 设置默认值了
解决一下:
// 使用方式一
@Component({})
export class DemoComponent {
@ObservableInput(true) // 自动绑定 name 值,即去除 `name$$` 末尾的 `$` 符号
@Input('name')
name$$: Observable<string>;
name: string;
}
// 使用方式二
@Component({})
export class DemoComponent {
@ObservableInput(true, 'Hello World') // 自动绑定 name Input 的值并设置默认值为 Hello World
name$$: Observable<string>;
@Input()
name: string;
}
// 使用方式三
@Component({})
export class DemoComponent {
@ObservableInput('nameValue') // 自动绑定 nameValue Input 的值
name$$: Observable<string>;
@Input()
nameValue: string;
}
即我们提供更加灵活的 ObservableInput 使用方式满足相对更多的使用需求。
本质上实现这样的数据劫持并不是什么黑魔法,只需要 ES5 环境支持(Symbol 可以换成其他实现):
基本实现
export function ObservableInput<
T = any,
SK extends keyof T = any,
K extends keyof T = any
>(propertyKey?: K | boolean, initialValue?: SubjectType<T[SK]>) {
return (target: T, sPropertyKey: SK) => {
const symbol = Symbol();
type ST = SubjectType<T[SK]>;
type Mixed = T & {
[symbol]: BehaviorSubject<ST>;
} & Record<SK, BehaviorSubject<ST>>;
Object.defineProperty(target, sPropertyKey, {
enumerable: true,
configurable: true,
get(this: Mixed) {
return (
this[symbol] || (this[symbol] = new BehaviorSubject<ST>(initialValue))
);
},
set(this: Mixed, value: ST) {
this[sPropertyKey].next(value);
},
});
if (!propertyKey) {
return;
}
if (propertyKey === true) {
propertyKey = (sPropertyKey as string).replace(/\$+$/, '') as K;
}
Object.defineProperty(target, propertyKey, {
enumerable: true,
configurable: true,
get(this: Mixed) {
return this[sPropertyKey].getValue();
},
set(this: Mixed, value: ST) {
this[sPropertyKey].next(value);
},
});
};
}
One more thing
使用类似的方案我们可以实现一个 ValueHook 装饰器来实现不需要多增加私有变量而自定义 Input 的 settter 和 getter:
@Component({})
export class DemoComponent {
@ValueHook(function(name) {
// do something
})
@Input()
name: string;
}
如果只是为了拦截 setter,ValueHook 的使用似乎更加有效。
基本实现
const checkDescriptor = <T, K extends keyof T>(target: T, propertyKey: K) => {
const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
if (descriptor && !descriptor.configurable) {
throw new TypeError(`property ${propertyKey} is not configurable`);
}
return {
oGetter: descriptor && descriptor.get,
oSetter: descriptor && descriptor.set,
};
};
export function ValueHook<T = any, K extends keyof T = any>(
setter?: (this: T, value?: T[K]) => boolean | void,
getter?: (this: T, value?: T[K]) => T[K],
) {
return (target: T, propertyKey: K) => {
const { oGetter, oSetter } = checkDescriptor(target, propertyKey);
const symbol = Symbol();
type Mixed = T & {
[symbol]: T[K];
};
Object.defineProperty(target, propertyKey, {
enumerable: true,
configurable: true,
get(this: Mixed) {
return getter
? getter.call(this, this[symbol])
: oGetter
? oGetter.call(this)
: this[symbol];
},
set(this: Mixed, value: T[K]) {
if (
value === this[propertyKey] ||
(setter && setter.call(this, value) === false)
) {
return;
}
if (oSetter) {
oSetter.call(this, value);
}
this[symbol] = value;
},
});
};
}
Last But Not Least
@ObservableInput 和 @ValueHook 实际上可以组合使用,但大部分情况下你没必要也不应该这么做,如果你有这种需求,可能你更应该重构一下代码了。:)
[zh]
What
如题,实现一个将 Angular 组件
Input自动转化为Observable的自定义拦截器:通过上面的
ObservableInput装饰器,我们将父组件传递的Inputname自动转化成了一个Observable对象。Why
Angular 组件中我们使用
@Input获取父组件传递的上下文数据,类似 React/Vue 中props的概念。通常我们为了支持Input动态变化并做出一些相关操作的情况,会将@Input定义为setter的方式,同时我们为了取到最新的Input值又需要定义一个内部私有变量和一个对应的getter:很明显,如果项目里的组件的
Input越来越多且我们都需要支持动态Input的话可能会有很多这样的模板代码,且类似_name这样的中间变量放在代码里既显得丑陋又影响代码阅读体验,而实际上 Angular 社区对ObservableInput的需求已经由来已久:Proposal: Input as Observable,但官方一直未提供相应的实现。How
目前社区里类似的 ObservableInput 实现也都是通过自定义
getter/settter劫持的方案来完成数据的转换,但是依然存在一些问题:Observable对象后无法直接获取原来Input的值了Input设置默认值了解决一下:
即我们提供更加灵活的
ObservableInput使用方式满足相对更多的使用需求。本质上实现这样的数据劫持并不是什么黑魔法,只需要 ES5 环境支持(Symbol 可以换成其他实现):
基本实现
One more thing
使用类似的方案我们可以实现一个
ValueHook装饰器来实现不需要多增加私有变量而自定义Input的settter和getter:如果只是为了拦截
setter,ValueHook的使用似乎更加有效。基本实现
Last But Not Least
@ObservableInput和@ValueHook实际上可以组合使用,但大部分情况下你没必要也不应该这么做,如果你有这种需求,可能你更应该重构一下代码了。:)