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 劫持的方案來完成數據的轉換,但是依然存在一些問題:

  1. 轉化成 Observable 對象後無法直接獲取原來 Input 的值了
  2. 無法給原始 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 裝飾器來實現不需要多增加私有變數而自定義 Inputsetttergetter

@Component({})
export class DemoComponent {
@ValueHook(function(name) {
// do something
})
@Input()
name: string;
}

如果只是為了攔截 setterValueHook 的使用似乎更加有效。

基本實現

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 實際上可以組合使用,但大部分情況下你沒必要也不應該這麼做,如果你有這種需求,可能你更應該重構一下代碼了。:)

推薦閱讀:

相关文章