巧用 TypeScript(二)
Decorator 早已不是什麼新鮮事物。在 TypeScript 1.5 + 的版本中,我們可以利用內建型別ClassDecorator
、PropertyDecorator
、MethodDecorator
與ParameterDecorator
更快書寫 Decorator,如MethodDecorator
:
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; 複製程式碼
使用時,只需在相應地方加上型別註解,匿名函式的引數型別也就會被自動推匯出來了。
function methodDecorator (): MethodDecorator { return (target, key, descriptor) => { // ... }; } 複製程式碼
值得一提的是,如果你在 Decorator 給目標類的 prototype 新增屬性時,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 來擴充套件一個類時。
GitHub 上有一個關於此問題的ofollow,noindex">issues ,直至目前,也沒有一個合適的方案實現它。其主要問題在於 TypeScript 並不知道目標類是否使用了 Decorator,以及 Decorator 的名稱。從這個issues 來看,建議的解決辦法是使用 Mixin:
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 時,這無疑是一件讓人頭大的事情。
這有一些偏方,能讓你順利從 JavaScript 遷移至 TypeScript:
-
顯式賦值斷言修飾符,即是在類裡,明確說明某些屬性存在於類上:
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 中:
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
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 複製程式碼
Controller 與 Get 的實現
如果你在使用 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 本身(控制反轉)。