TypeScript装饰器整理及用例介绍
整理下TS装饰器(Decorator)的相关内容。
- 装饰器种类
- 装饰器工厂与组合使用
- metadata reflecion API
- 开源库中的使用
下面使用的TypeScript版本为3.6.3
为何用装饰器
- 分离出通用的属性配置与函数功能
- 增强代码可读性
- 尝试新特性
装饰器种类
装饰器/属性 | 类装饰器 | 方法装饰器 | 访问器装饰器 | 属性装饰器 | 参数装饰器 |
---|---|---|---|---|---|
位置 | @foo class Bar {} | @foo public bar() {} | @foo get bar() | @foo() bar: number | bar(@foo para: string) {} |
传入参数 | constructor | target, propertyKey, descriptor | target, propertyKey, descriptor | target, propertyKey | target, propertyKey, parameterIndex |
返回值 | 用返回值提供的构造函数来替换类的声明 | 返回值被用作方法的属性描述符 | 返回值被用作方法的属性描述符 | 被忽略 | 被忽略 |
参数释义:
constructor
: 类构造函数target
: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。propertyKey
: 成员的名字descriptor
: 成员的属性描述符parameterIndex
: 参数在函数参数列表中的索引
一个示例
类的声明与调用部分
@fooClassDcrt
class Student {
constructor(name) {
this.name = name;
}
@fooPropDcrt('female')
sex: string;
@fooAccessorDcrt(false)
get name: string;
@fooMethodDcrt('Good morning')
say(@fooParaDcrt words) {
console.log(words)
}
}
const lihua = new Student('lihua')
lihua.sex = 'female'
lihua.say('hello decorator') // => Say hello decorator from fooMethodDcrt\n hello decorator
console.log(lihua.type) // => Student
装饰器部分
// 类装饰器
function fooClassDcrt<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
type="Student"
}
}
// 属性装饰器
function fooPropDcrt(value: string) {
return (target: any, propertyKey: string | symbol) => {
console.log(`Prop::${propertyKey} meta value: ${value}`)
}
}
// 访问器装饰器
function fooAccessorDcrt(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
// 方法装饰器
function fooMethodDcrt(value: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const method = descriptor.value
descriptor.value = function (...args) {
console.log(`Say ${value} from fooMethodDcrt`)
method.apply(this, [...args])
}
return descriptor
}
}
// 参数装饰器
function fooParaDcrt() {
return function (target: any, propertyKey: string, parameterIndex) {
console.log(propertyKey, parameterIndex)
}
}
装饰器工厂
TS官方提案的描述:A decorator factory is a function that can accept any number of arguments, and must return one of the types of decorator.
传入任意数量的参数并返回一种装饰器,即一种装饰器函数构造器。可以在返回执行的装饰器方法前对传入数据进行一定处理并在装饰器中使用。
返回不同类型的装饰器
假如有一类具有同样记录数据的功能但不同类型的装饰器们:
@logClass
class foo {
@logProp public bar: string;
@logMethod handler(@logParam num: number) {}
}
虽然上述方式可以正常工作,但如果统一他们的调用会更加易用,像下面这样:
@log
class foo {
@log public bar: string;
@log handler(@log num: number) {}
}
这可以通过用一个装饰器工厂包裹不同装饰器函数来实现:
function log(...args : any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if(typeof args[2] === "number") {
return logParam.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}
传入自定义参数
一个普通的方法装饰器
class foo {
@injectMethod
bar() {};
}
function injectMethod(target, propertyKey, descriptor) {
// 其他描述符与属性处理
return descriptor
}
一个传参的方法装饰器
class foo {
@logMethod('Hello world!')
bar() {};
}
function logMethod (message: string) {
return function(target, propertyKey, descriptor) {
const method = descriptor.value
descriptor.value = function (...args) {
// 在装饰器工厂返回的函数中使用所传参数
console.log(`Print: ${message}`)
// 其他描述符与属性处理
method.apply(this, ...args)
}
return descriptor
}
}
组合使用
在同一目标上使用多个装饰器时的处理顺序:
- 参数计算:从上至下
- 装饰器执行:从下至上
示例:
function foo() {
console.log("foo(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("foo(): called");
}
}
function bar() {
console.log("bar(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("bar(): called");
}
}
class C {
@foo()
@bar()
method() {}
}
// => foo(): evaluated
// => bar(): evaluated
// => bar(): called
// => foo(): called
metadata reflection API
反射(Reflect)一般指那些用来检验(inspect)其他代码的代码,部分使用场景:依赖注入/运行时类型断言/测试。
当JS应用变复杂时,需要使用一些(IoC容器)或特性(运行时类型断言)来处理复杂度,而由于JS中不存在反射的特性很难做到这一点。
一个反射API应允许在运行时检验一个未知对象,获取其不限于名称、类型、接口、参数等信息。
在JS中可以使用Object.getOwnPropertyDescriptor()
或Object.keys()
来获取实体的信息,但还是需要反射去实现更强大的开发工具。目前在TS中可以使用reflect-metadata来实现反射特性,同时这个库是作为metadata reflection API的polyfill,可参考相关提案。
在实现装饰器时可以使用如下几种design keys来获取反射元信息:
-
design:type
- 类型元信息import 'reflec-metadata' const logType = (value: string) => { return function(target: any, propertyKey: string) => { const type = Reflect.getMetadata("design:type", target, propertyKey); console.log(`${value} - ${propertyKey} type: ${type.name}`); } } class Foo { @logType('Prop decorator') public barProp: string; } // => Prop decorator - barProp type: String
-
design:paramtypes
- 参数类型元信息import 'reflec-metadata' const logParamTypes = (target: any, propertyKey: string, descriptor) => { const types = Reflect.getMetadata("design:paramtypes", target, propertyKey); const s = types.map(type => type.name).join(',') console.log(`${propertyKey} param types: ${s}`); } class Foo { @logParamTypes barMethod(name: string, age: number) { } } // => barMethod param types: String,Number
-
design:returntype
- 返回值元信息import 'reflec-metadata' const logReturnTypes = (target: any, propertyKey: string, descriptor) => { const type = Reflect.getMetadata("design:returntype", target, propertyKey); console.log(`${propertyKey} return types: ${type.name}`); } class Foo { @logReturnTypes barMethod(age: number) : number { return age } } // => barMethod return types: Number
除了通过内置的design key获取外,也可以自定义元信息,以验证方法参数的场景为例:
-
定义校验方法参数的方法装饰器
const requiredMetadataKey = Symbol("required"); const CheckParams = (target: any, propertyKey: string, descriptor) => { let method = descriptor.value; descriptor.value = function () { let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey); requiredParameters.map(index => { if(arguments[index] === undefined) throw new Error('Missing required parameter') }) return method.apply(this, arguments); } }
-
定义设置元信息的参数装饰器
const required = (target: any, propertyKey: string, parameterIndex: number) => { let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey); }
-
验证是否提供指定参数
class Foo{ @CheckParams bar(@required name: string, @required age: number, sex: string) { } } (new Foo()).bar('hello') // => Error: Missing required parameter
开源库中的使用
vue: vue-class-component & vue-property-decorator
如果使用ts编写vue项目并使用类组件的话少不了vue-class-component与vue-property-decorator,其中提供了vue相关组件与属性的装饰器,来看看它们的使用与实现。
在class类型的Vue组件中使用装饰器:
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component
export default class FooComponent extends Vue {
@Prop(Number) fooProp: number
}
首先是底层的装饰器类型与装饰器工厂的辅助方法:
// vue-class-component/src/utils.ts
// 三种装饰器类型接口
export interface VueDecorator {
// Class decorator
(Ctor: typeof Vue): void
// Property decorator
(target: Vue, key: string): void
// Parameter decorator
(target: Vue, key: string, index: number): void
}
// 装饰器工厂的辅助方法
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
return (target: Vue | typeof Vue, key?: any, index?: any) => {
// 在类构造函数的属性中保存装饰器的用于数据同步等方法(factory)
const Ctor = typeof target === 'function'
? target as DecoratedClass
: target.constructor as DecoratedClass
if (!Ctor.__decorators__) {
Ctor.__decorators__ = []
}
if (typeof index !== 'number') {
index = undefined
}
Ctor.__decorators__.push(options => factory(options, key, index))
}
}
其次是Compnent类装饰器工厂方法:
export function componentFactory (
Component: VueClass<Vue>,
options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
// 1. 解析原型的属性及方法等,赋值到options上
const proto = Component.prototype
Object.getOwnPropertyNames(proto).forEach(function (key) {...}
// 2. 添加data hook收集类属性数据
;(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return collectDataFromConstructor(this, Component)
}
})
// 3. 装饰器属性处理
const decorators = (Component as DecoratedClass).__decorators__
if (decorators) {
// 执行已注册的装饰器
decorators.forEach(fn => fn(options))
delete (Component as DecoratedClass).__decorators__
}
// 4. 结合父类与options生成构造函数并返回
const superProto = Object.getPrototypeOf(Component.prototype)
const Super = superProto instanceof Vue
? superProto.constructor as VueClass<Vue>
: Vue
const Extended = Super.extend(options)
return Extended
}
Prop等属性装饰器工厂就比较简单了:
// vue-property-ecorator/src/vue-property-decorator.ts
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
return (target: Vue, key: string) => {
// 利用Reflect处理错误的类型问题
applyMetadata(options, target, key)
// 与target同步options中的属性配置,并保存该操作(__decorators__)
createDecorator((componentOptions, k) => {
;(componentOptions.props || ((componentOptions.props = {}) as any))[ k ] = options
})(target, key)
}
}
可以看出,Vue类类型组件相关的装饰器函数会将不同方法与属性配置同步到父类的构造器上,即在实例化前注入相关属性。
server: routing-controllers
routing-controllers的装饰器作用与vue的类似,但它使用了另一种实现方式。
import { Controller, Body, Post } from "routing-controllers";
@Controller()
export class UserController {
@Post("/users")
post(@Body() user: any) {
return "Saving user...";
}
}
控制器类装饰器:
export function Controller(baseRoute?: string): Function {
return function (object: Function) {
getMetadataArgsStorage().controllers.push({
type: "default",
target: object,
route: baseRoute
});
};
}
可以看出它使用了一个getMetadataArgsStorage
的辅助方法,该方法会返回一个MetadataArgsStorage
实例,该实例中保存注册的所有类型的metadata,且该实例会绑定到全局对象上,可随时访问所有metadata。
export class MetadataArgsStorage {
// 保存构建metadata实例所需的args对象数组
controllers: ControllerMetadataArgs[] = [];
middlewares: MiddlewareMetadataArgs[] = [];
actions: ActionMetadataArgs[] = [];
...
// 过滤已存在类的参数对象
filterControllerMetadatasForClasses(classes: Function[]): ControllerMetadataArgs[] {
return this.controllers.filter(ctrl => {
return classes.filter(cls => ctrl.target === cls).length > 0;
});
}
...
}
其中metadata实例会用于在创建Server时内部RoutingControllers的组件注册。
可以看出,这些装饰器函数中主要执行了实例化前的参数准备,使用独立的对象保存实参,便于最终实例化时的参数解析、服务注册与对象生成。
不仅是controller的类装饰器会预存实参,HTTP的方法装饰器与参数装饰器同样,如下所示:
export function Post(route?: string|RegExp): Function {
return function (object: Object, methodName: string) {
getMetadataArgsStorage().actions.push({
type: "post",
target: object.constructor,
method: methodName,
route: route
});
};
}
export function Body(options?: BodyOptions): Function {
return function (object: Object, methodName: string, index: number) {
getMetadataArgsStorage().params.push({
type: "body",
object: object,
method: methodName,
index: index,
parse: false,
required: options ? options.required : undefined,
classTransform: options ? options.transform : undefined,
validate: options ? options.validate : undefined,
explicitType: options ? options.type : undefined,
extraOptions: options ? options.options : undefined
});
};
}
在mobx(状态管理),typedi(IoC)等库中也大量使用了装饰器,用法也各有不同,感兴趣的了解下。
参考
- 原文作者:yrq110
- 原文链接:http://yrq110.me/post/front-end/typescript-decorator-practice/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。