Typescript 装饰器和反射

装饰器

装饰器(也叫注解)对一个类/方法/属性/参数的装饰。它是对这一系列代码的增强,并且通过自身描述了被装饰的代码可能存在行为改变。

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器分为:类装饰器、方法装饰器、属性装饰器、参数装饰器

要使用装饰器需要在tsconfig.json中配置:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

一个简单的示例:

在一个User类中含有一个changeName方法,在调用方法前,需要调用validate方法进行校验,此种方式缺点:侵入了changeName主流程代码,并且耦合性提高

class User {
    name: string;
 
    id: number;
 
    constructor(name: string, id: number) {
        this.name = name;
        this.id = id;
    }
 
    public validate(newName: string) {
        if (!newName) {
            throw Error("name is invalid");
        }
    }
 
    public changeName(newName: string) {
        this.validate(newName)
        this.name = newName;
    }
}

借助装饰器可以进行以下改造,不侵入代码,降低耦合度

const validate = (target: any,propertyKey: string,descriptor: PropertyDescriptor) => {
    let method = descriptor.value;
    descriptor.value = function(newValue: string) {
        if (!newValue) {
            throw Error("name is invalid");
        } else {
            method.apply(this, arguments)
        }
    }
}
class User {
    name: string;
 
    id: number;
 
 
    constructor(name: string, id: number) {
        this.name = name;
        this.id = id;
    }
 
    public validate(newName: string) {
        if (!newName) {
            throw Error("name is invalid");
        }
    }
 
    @validate
    public changeName(newName: string) {
        this.name = newName;
    }
}

装饰器工厂 - 可以传参的装饰器

function print(msg: string) {
    return function(traget: string, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log(`msg: ` + msg)
    }
}
class User {
    @print('getName evaluated')
    public getName () {
        return 'name'
    }
}
 
// 调用getName时会输出: getName evaluated
类装饰器

类装饰器表达式会在运行时当做函数被调用,类的构造函数作为其唯一的参数

const sealed = (constructor: Function) {
    Object.seal(constructor)
    Object.seal(constructor.prototype)
}
 
@sealed
class User{}
方法装饰器

方法装饰器会在运行时当做函数被调用,有3个参数:

1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

2、成员的名字

3、成员的属性描述符{ value: any, writable: boolean, enumerable: boolean, configurable: boolean }

示例参考示例1

访问器装饰器

和函数装饰器一样,只不过是装饰与访问器上

function print(msg: string) {
    return function(traget: string, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log(`msg: ` + msg)
    }
}
class User {
    private _name: string
    @print('getName evaluated')
    get name () {
        return this._name
    }
}
属性装饰器

属性装饰器会在运行时当做函数被调用,有2个参数
1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
2、成员的名字

const freeze = (target, propertyKey) => {
    target[propertyKey] = 0;
    Object.defineProperty(target, propertyKey, {
        configurable: false,
        writable: false,
    });
}
class User {
    @freeze
    id: number
}
参数装饰器

参数装饰器会在运行时当作函数被调用,有3个参数
1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
2、成员的名字
3、参数在函数参数列表的索引

const required = function (target: any, propertyKey: string, paramterIndex: number) {
    console.log(target[propertyKey].prototype)
}
class User {
    private _name: string
    changeName (@required name: string) {
        this._name = name
    }
}

validate示例用在参数装饰器上

// 定义一个私有 key
const requiredMetadataKey = Symbol("required")
// 定义参数装饰器,大概思路就是把要校验的参数索引保存到成员中
const required = function (target, propertyKey: string, parameterIndex: number) {
    // 参数装饰器只能拿到参数的索引
    if (!target[propertyKey][requiredMetadataKey]) {
        target[propertyKey][requiredMetadataKey] = {}
    }
    // 把这个索引挂到属性上
    target[propertyKey][requiredMetadataKey][parameterIndex] = true
}
 
// 定义一个方法装饰器,从成员中获取要校验的参数进行校验
const validate = function (target, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原来的方法
    let method = descriptor.value
    // 重写原来的方法
    descriptor.value = function () {
        let args = arguments
        // 看看成员里面有没有存的私有的对象
        if (target[propertyKey][requiredMetadataKey]) {
            // 检查私有对象的 key
            Object.keys(target[propertyKey][requiredMetadataKey]).forEach(parameterIndex => {
            // 对应索引的参数进行校验
                if (!args[parameterIndex]) throw Error(`arguments${parameterIndex} is invalid`)
            })
        }
    }
}
 
class User {
    name: string
    id: number
    constructor(name:string, id: number) {
        this.name = name
        this.id = id
    }
 
    // 方法装饰器做校验
    @validate
    changeName (@required newName: string) { // 参数装饰器做描述
        this.name = newName
    }
}

反射

反射实在运行时动态获取一个对象的一切信息:方法/属性等等,特点在于动态类型反推导,在typescript中,反射的原理是通过设计阶段对对象注入元数据信息,在运行阶段读取注入的元数据,从而得到对象信息。

使用时需要在tsconfig.json配置并且引入reflect-metadata

{
    "compilerOptions": {
        "target": "ES5",
        "emitDecoratorMetadata": true
    }
}

反射可以获取对象的:
1、对象的类型
2、对象/静态属性的信息(类型)
3、方法的参数类型,返回类型

默认注入的三种元数据:
1、design:type:成员类型
2、design:paramtypes:成员所有参数类型
3、design:returntype:成员返回类型
使用反射实现动态路由和自动注入生成

默认元数据示例
import "reflect-metadata";

const Prop = (): PropertyDecorator => {
    return (target, key: string) => {
        const _type = Reflect.getMetadata('design:type', target, key)
        console.log("type: ", _type) // type: [Function: String]
    }
}

const Method = (): MethodDecorator => {
    return (traget: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        const _params = Reflect.getMetadata('design:paramtypes', traget, propertyKey)
        console.log('params: ', _params) // params:  [ [Function: String] ]
    }
}

const GetReturn = (): MethodDecorator => {
    return (traget: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        const _return = Reflect.getMetadata('design:returntype', traget, propertyKey)
        console.log('return type: ', _return) // return type:  [Function: String]
    }
}

class TestReflect {
    @Prop()
    public name: String

    @GetReturn()
    public getName (): String {
        return this.name
    }

    @Method()
    public setName (name: String) {
        this.name = name
    }
}

上面示例拿到了返回,类型和参数,就可以做些略微高级的事

简单的自动注入
import 'reflect-metadata'
const Autowired = (target: any, key: string) => {
	const type = Reflect.getMetadata('design:type', target, key)
    return target[key] = new type()
}
 
class ServiceDemo {
    public getService () {
        console.log('aaa')
    }
}
 
class ReflectDemo {
 
    @Autowired
    serviceDemo: ServiceDemo
 
 
    testAutowired () {
        this.serviceDemo.getService()
    }
}
 
let reflectDemo = new ReflectDemo()
reflectDemo.testAutowired() // aaa
自动生成路由
import 'reflect-metadata'
 
const Prefix = (path: string): ClassDecorator => {
    return (target: any) => {
        Reflect.defineMetadata(Symbol.for('PREFIX'), path, target)
    }
}
 
const getMethodDecorator = (method: string) => (path: string): MethodDecorator => {
    return (target: any, key: string, descriptor: PropertyDescriptor) => {
        Reflect.defineMetadata(Symbol.for('PATH_METADATA'), path, descriptor.value)
        Reflect.defineMetadata(Symbol.for('METHOD_METADATA'), method, descriptor.value)
    }
}
 
const Get = getMethodDecorator('get')
const Post = getMethodDecorator('post')
 
 
@Prefix('/test')
class ReflectDemo {
 
    @Get('/get-method')
    getMethod () {
    }
 
    @Post('/post-method')
    postMethod () {
 
    }
 
}
 
const mapRoutes = (instance: Object) => {
    const prototype = Object.getPrototypeOf(instance)
 
    const methodNames = Object.getOwnPropertyNames(prototype).filter(item => item !== 'constructor' && item !== 'Function')
 
    const prefix = Reflect.getMetadata(Symbol.for('PREFIX'), prototype['constructor'])
    return methodNames.map(methodName => {
        const fn = prototype[methodName]
        if (isFunction(fn) {
            const route = Reflect.getMetadata(Symbol.for('PATH_METADATA'), fn)
            const method = Reflect.getMetadata(Symbol.for('METHOD_METADATA'), fn)
            return {
                route,
                method,
                fn,
                methodName
            }
        }
    })
}
 
mapRoutes(new ReflectDemo())

参考

从 JavaScript 到 TypeScript 4 - 装饰器和反射

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值