如下問題來自於與公司小夥伴以及網友的討論,整理成章,但願提供另外一種思路(避免踩坑)解決問題。vue
Decorator 早已不是什麼新鮮事物。在 TypeScript 1.5 + 的版本中,咱們能夠利用內置類型 ClassDecorator
、PropertyDecorator
、MethodDecorator
與 ParameterDecorator
更快書寫 Decorator,如 MethodDecorator
:git
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
複製代碼
使用時,只需在相應地方加上類型註解,匿名函數的參數類型也就會被自動推導出來了。github
function methodDecorator (): MethodDecorator {
return (target, key, descriptor) => {
// ...
};
}
複製代碼
值得一提的是,若是你在 Decorator 給目標類的 prototype 添加屬性時,TypeScript 並不知道這些:typescript
function testAble(): ClassDecorator {
return target => {
target.prototype.someValue = true
}
}
@testAble()
class SomeClass {}
const someClass = new SomeClass()
someClass.someValue() // Error: Property 'someValue' does not exist on type 'SomeClass'.
複製代碼
這很常見,特別是當你想用 Decorator 來擴展一個類時。express
GitHub 上有一個關於此問題的 issues,直至目前,也沒有一個合適的方案實現它。其主要問題在於 TypeScript 並不知道目標類是否使用了 Decorator,以及 Decorator 的名稱。從這個 issues 來看,建議的解決辦法是使用 Mixin:npm
type Constructor<T> = new(...args: any[]) => T
// mixin 函數的聲明,須要實現
declare function mixin<T1, T2>(...MixIns: [Constructor<T1>, Constructor<T2>]): Constructor<T1 & T2>;
class MixInClass1 {
mixinMethod1() {}
}
class MixInClass2 {
mixinMethod2() {}
}
class Base extends mixin(MixInClass1, MixInClass2) {
baseMethod() { }
}
const x = new Base();
x.baseMethod(); // OK
x.mixinMethod1(); // OK
x.mixinMethod2(); // OK
x.mixinMethod3(); // Error
複製代碼
當把大量的 JavaScript Decorator 重構爲 Mixin 時,這無疑是一件讓人頭大的事情。json
這有一些偏方,能讓你順利從 JavaScript 遷移至 TypeScript:segmentfault
顯式賦值斷言修飾符,便是在類裏,明確說明某些屬性存在於類上:app
function testAble(): ClassDecorator {
return target => {
target.prototype.someValue = true
}
}
@testAble()
class SomeClass {
public someValue!: boolean;
}
const someClass = new SomeClass();
someClass.someValue // true
複製代碼
採用聲明合併形式,單獨定義一個 interface,把用 Decorator 擴展的屬性的類型,放入 interface 中:koa
interface SomeClass {
someValue: boolean;
}
function testAble(): ClassDecorator {
return target => {
target.prototype.someValue = true
}
}
@testAble()
class SomeClass {}
const someClass = new SomeClass();
someClass.someValue // true
複製代碼
Reflect Metadata 是 ES7 的一個提案,它主要用來在聲明的時候添加和讀取元數據。TypeScript 在 1.5+ 的版本已經支持它,你只須要:
npm i reflect-metadata --save
。tsconfig.json
裏配置 emitDecoratorMetadata
選項。它具備諸多使用場景。
譬如在 vue-property-decorator
6.1 及其如下版本中,經過使用 Reflect.getMetadata
API,Prop
Decorator 能獲取屬性類型傳至 Vue,簡要代碼以下:
function Prop(): PropertyDecorator {
return (target, key: string) => {
const type = Reflect.getMetadata('design:type', target, key);
console.log(`${key} type: ${type.name}`);
// other...
}
}
class SomeClass {
@Prop()
public Aprop!: string;
};
複製代碼
運行代碼可在控制檯看到 Aprop type: string
。除能獲取屬性類型外,經過 Reflect.getMetadata("design:paramtypes", target, key)
和 Reflect.getMetadata("design:returntype", target, key)
能夠分別獲取函數參數類型和返回值類型。
metadataKey
除能獲取類型信息外,經常使用於自定義 metadataKey
,並在合適的時機獲取它的值,示例以下:
function classDecorator(): ClassDecorator {
return target => {
// 在類上定義元數據,key 爲 `classMetaData`,value 爲 `a`
Reflect.defineMetadata('classMetaData', 'a', target);
}
}
function methodDecorator(): MethodDecorator {
return (target, key, descriptor) => {
// 在類的原型屬性 'someMethod' 上定義元數據,key 爲 `methodMetaData`,value 爲 `b`
Reflect.defineMetadata('methodMetaData', 'b', target, key);
}
}
@classDecorator()
class SomeClass {
@methodDecorator()
someMethod() {}
};
Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'
複製代碼
在 Angular 2+ 的版本中,控制反轉與依賴注入即是基於此實現,如今,咱們來實現一個簡單版:
type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => target => {}
class OtherService {
a = 1
}
@Injectable()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 獲取全部注入的服務
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args);
}
Factory(TestService).testMethod() // 1
複製代碼
若是你在使用 TypeScript 開發 Node 應用,相信你對 Controller
、Get
、POST
這些 Decorator,並不陌生:
@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world';
}
@Post('/b')
somePostMethod() {}
};
複製代碼
它們也是基於 Reflect Metadata
實現,不一樣的是,此次咱們將 metadataKey
定義在 descriptor
的 value
上(稍後解釋),簡單實現以下:
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';
const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
}
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');
複製代碼
接着,建立一個函數,映射出 route
:
function mapRoute(instance: Object) {
const prototype = Object.getPrototypeOf(instance);
// 篩選出類的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]));
return methodsNames.map(methodName => {
const fn = prototype[methodName];
// 取出定義的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn);
const method = Reflect.getMetadata(METHOD_METADATA, fn);
return {
route,
method,
fn,
methodName
}
})
};
複製代碼
咱們能夠獲得一些有用的信息:
Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'
mapRoute(new SomeClass())
/** * [{ * route: '/a', * method: 'GET', * fn: someGetMethod() { ... }, * methodName: 'someGetMethod' * },{ * route: '/b', * method: 'POST', * fn: somePostMethod() { ... }, * methodName: 'somePostMethod' * }] * */
複製代碼
最後,只需把 route
相關信息綁在 express
或者 koa
上就 ok 了。
至於爲何要定義在 descriptor
的 value
上,咱們但願 mapRoute
函數的參數是一個實例,而非 class 自己(控制反轉)。