zoukankan      html  css  js  c++  java
  • 搭建node服务(四):Decorator装饰器

    Decorator(装饰器)是ECMAScript中一种与class相关的语法,用于给对象在运行期间动态的增加功能。Node.js还不支持Decorator,可以使用Babel进行转换,也可以在TypeScript中使用Decorator。本示例则是基于TypeScript来介绍如何在node服务中使用Decorator。

    一、 TypeScript相关

    由于使用了 TypeScript ,需要安装TypeScript相关的依赖,并在根目录添加 tsconfig.json 配置文件,这里不再详细说明。要想在 TypeScript 中使用Decorator 装饰器,必须将 tsconfig.json 中 experimentalDecorators设置为true,如下所示:

    tsconfig.json

    {
      "compilerOptions": {
        …
        // 是否启用实验性的ES装饰器
        "experimentalDecorators": true
      }
    }
    

    二、 装饰器介绍

    1. 简单示例

    Decorator实际是一种语法糖,下面是一个简单的用TypeScript编写的装饰器示例:

    
    const Controller: ClassDecorator = (target: any) => {
        target.isController = true;
    };
    
    @Controller
    class MyClass {
    
    }
    
    console.log(MyClass.isController); // 输出结果:true
    

    Controller是一个类装饰器,在MyClass类声明前以 @Controller 的形式使用装饰器,添加装饰器后MyClass. isController 的值为true。
    编译后的代码如下:

    var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
        var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
        if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
        else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
        return c > 3 && r && Object.defineProperty(target, key, r), r;
    };
    
    const Controller = (target) => {
        target.isController = true;
    };
    let MyClass = class MyClass {
    };
    MyClass = __decorate([
        Controller
    ], MyClass);
    

    2. 工厂方法

    在使用装饰器的时候有时候需要给装饰器传递一些参数,这时可以使用装饰器工厂方法,示例如下:

    function controller ( label: string): ClassDecorator {
        return (target: any) => {
            target.isController = true;
            target.controllerLabel = label;
        };
    }
    
    @controller('My')
    class MyClass {
    
    }
    
    console.log(MyClass.isController); // 输出结果为: true
    console.log(MyClass.controllerLabel); // 输出结果为: "My"
    

    controller 方法是装饰器工厂方法,执行后返回一个类装饰器,通过在MyClass类上方以 @controller('My') 格式添加装饰器,添加后 MyClass.isController 的值为true,并且MyClass.controllerLabel 的值为 "My"。

    3. 类装饰器

    类装饰器的类型定义如下:

    type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
    

    类装饰器只有一个参数target,target为类的构造函数。
    类装饰器的返回值可以为空,也可以是一个新的构造函数。
    下面是一个类装饰器示例:

    interface Mixinable {
        [funcName: string]: Function;
    }
    function mixin ( list: Mixinable[]): ClassDecorator {
        return (target: any) => {
            Object.assign(target.prototype, ...list)
        }
    }
    
    const mixin1 = {
        fun1 () {
            return 'fun1'
        }
    };
    
    const mixin2 = {
        fun2 () {
            return 'fun2'
        }
    };
    
    @mixin([ mixin1, mixin2 ])
    class MyClass {
    
    }
    
    console.log(new MyClass().fun1()); // 输出:fun1
    console.log(new MyClass().fun2()); // 输出:fun2
    

    mixin是一个类装饰器工厂,使用时以 @mixin() 格式添加到类声明前,作用是将参数数组中对象的方法添加到 MyClass 的原型对象上。

    4. 属性装饰器

    属性装饰器的类型定义如下:

    type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
    

    属性装饰器有两个参数 target 和 propertyKey。

    • target:静态属性是类的构造函数,实例属性是类的原型对象
    • propertyKey:属性名

    下面是一个属性装饰器示例:

    interface CheckRule {
        required: boolean;
    }
    interface MetaData {
        [key: string]: CheckRule;
    }
    
    const Required: PropertyDecorator = (target: any, key: string) => {
        target.__metadata = target.__metadata ? target.__metadata : {};
        target.__metadata[key] = { required: true };
    };
    
    class MyClass {
        @Required
        name: string;
        
        @Required
        type: string;
    }
    

    @Required 是一个属性装饰器,使用时添加到属性声明前,作用是在 target 的自定义属性__metadata中添加对应属性的必填规则。上例添加装饰器后target.__metadata 的值为:{ name: { required: true }, type: { required: true } }。
    通过读取 __metadata 可以获得设置的必填的属性,从而对实例对象进行校验,校验相关的代码如下:

    function validate(entity): boolean {
        // @ts-ignore
        const metadata: MetaData = entity.__metadata;
        if(metadata) {
            let i: number,
                key: string,
                rule: CheckRule;
            const keys = Object.keys(metadata);
            for (i = 0; i < keys.length; i++) {
                key = keys[i];
                rule = metadata[key];
                if (rule.required && (entity[key] === undefined || entity[key] === null || entity[key] === '')) {
                    return false;
                }
            }
        }
        return true;
    }
    
    const entity: MyClass = new MyClass();
    entity.name = 'name';
    const result: boolean = validate(entity);
    console.log(result); // 输出结果:false
    

    5. 方法装饰器

    方法装饰器的类型定义如下:

    type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
    

    方法装饰器有3个参数 target 、 propertyKey 和 descriptor。

    • target:静态方法是类的构造函数,实例方法是类的原型对象
    • propertyKey:方法名
    • descriptor:属性描述符
      方法装饰器的返回值可以为空,也可以是一个新的属性描述符。
      下面是一个方法装饰器示例:
    const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
        const className = target.constructor.name;
        const oldValue = descriptor.value;
        descriptor.value = function(...params) {
            console.log(`调用${className}.${key}()方法`);
            return oldValue.apply(this, params);
        };
    };
    
    class MyClass {
        private name: string;
    
        constructor(name: string) {
            this.name = name;
        }
    
        @Log
        getName (): string {
            return 'Tom';
        }
    }
    
    const entity = new MyClass('Tom');
    const name = entity.getName();
    // 输出: 调用MyClass.getName()方法
    
    

    @Log 是一个方法装饰器,使用时添加到方法声明前,用于自动输出方法的调用日志。方法装饰器的第3个参数是属性描述符,属性描述符的value表示方法的执行函数,用一个新的函数替换了原来值,新的方法还会调用原方法,只是在调用原方法前输出了一个日志。

    6. 访问符装饰器

    访问符装饰器的使用与方法装饰器一致,参数和返回值相同,只是访问符装饰器用在访问符声明之前。需要注意的是,TypeScript不允许同时装饰一个成员的get和set访问符。下面是一个访问符装饰器的示例:

    
    const Enumerable: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
        descriptor.enumerable = true;
    };
    
    class MyClass {
        createDate: Date;
        constructor() {
            this.createDate = new Date();
        }
    
        @Enumerable
        get createTime () {
            return this.createDate.getTime();
        }
    }
    
    const entity = new MyClass();
    for(let key in entity) {
        console.log(`entity.${key} =`, entity[key]);
    }
    /* 输出:
    entity.createDate = 2020-04-08T10:40:51.133Z
    entity.createTime = 1586342451133
     */
    
    

    MyClass 类中有一个属性createDate 为Date类型, 另外增加一个有 get 声明的createTime方法,就可以以 entity.createTime 方式获得 createDate 的毫秒值。但是 createTime 默认是不可枚举的,通过在声明前增加 @Enumerable 装饰器可以使 createTime 成为可枚举的属性。

    7. 参数装饰器

    参数装饰器的类型定义如下:

    type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
    

    参数装饰器有3个参数 target 、 propertyKey 和 descriptor。

    • target:静态方法的参数是类的构造函数,实例方法的参数是类的原型对象
    • propertyKey:参数所在方法的方法名
    • parameterIndex:在方法参数列表中的索引值
      在上面 @Log 方法装饰器示例的基础上,再利用参数装饰器对添加日志的功能进行扩展,增加参数信息的日志输出,代码如下:
    function logParam (paramName: string = ''): ParameterDecorator  {
        return (target: any, key: string, paramIndex: number) => {
            if (!target.__metadata) {
                target.__metadata = {};
            }
            if (!target.__metadata[key]) {
                target.__metadata[key] = [];
            }
            target.__metadata[key].push({
                paramName,
                paramIndex
            });
        }
    }
    
    const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
        const className = target.constructor.name;
        const oldValue = descriptor.value;
        descriptor.value = function(...params) {
            let paramInfo = '';
            if (target.__metadata && target.__metadata[key]) {
                target.__metadata[key].forEach(item => {
                    paramInfo += `
     * 第${item.paramIndex}个参数${item.paramName}的值为: ${params[item.paramIndex]}`;
                })
            }
            console.log(`调用${className}.${key}()方法` + paramInfo);
            return oldValue.apply(this, params);
        };
    };
    
    class MyClass {
        private name: string;
    
        constructor(name: string) {
            this.name = name;
        }
    
        @Log
        getName (): string {
            return 'Tom';
        }
    
        @Log
        setName(@logParam() name: string): void {
            this.name = name;
        }
    
        @Log
        setNames( @logParam('firstName') firstName: string, @logParam('lastName') lastName: string): void {
            this.name = firstName + '' + lastName;
        }
    }
    
    const entity = new MyClass('Tom');
    const name = entity.getName();
    // 输出:调用MyClass.getName()方法
    
    entity.setName('Jone Brown');
    /* 输出:
    调用MyClass.setNames()方法
     * 第0个参数的值为: Jone Brown
    */
    
    entity.setNames('Jone', 'Brown');
    /* 输出:
    调用MyClass.setNames()方法
     * 第1个参数lastName的值为: Brown
     * 第0个参数firstName的值为: Jone
    */
    

    @logParam 是一个参数装饰器,使用时添加到参数声明前,用于输出参数信息日志。

    8. 执行顺序

    不同声明上的装饰器将按以下顺序执行:

    1. 实例成员的装饰器:
      参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器
    2. 静态成员的装饰器:
      参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器
    3. 构造函数的参数装饰器
    4. 类装饰器

    如果同一个声明有多个装饰器,离声明越近的装饰器越早执行:

    const A: ClassDecorator = (target) => {
        console.log('A');
    };
    
    const B: ClassDecorator = (target) => {
        console.log('B');
    };
    
    @A
    @B
    class MyClass {
    
    }
    
    /* 输出结果:
    B
    A
    */
    

    三、 Reflect Metadata

    1. 安装依赖

    Reflect Metadata是的一个实验性接口,可以通过装饰器来给类添加一些自定义的信息。这个接口目前还不是 ECMAScript 标准的一部分,需要安装 reflect-metadata垫片才能使用。

    npm install reflect-metadata --save
    

    或者

    yarn add reflect-metadata
    

    另外,还需要在全局的位置导入此模块,例如:入口文件。

    import 'reflect-metadata';
    

    2. 相关接口

    Reflect Metadata 提供的接口如下:

    // 定义元数据
    Reflect.defineMetadata(metadataKey, metadataValue, target);
    Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);
    
    // 检查指定关键字的元数据是否存在,会遍历继承链
    let result1 = Reflect.hasMetadata(metadataKey, target);
    let result2 = Reflect.hasMetadata(metadataKey, target, propertyKey);
    
    // 检查指定关键字的元数据是否存在,只判断自己的,不会遍历继承链
    let result3 = Reflect.hasOwnMetadata(metadataKey, target);
    let result4 = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);
    
    // 获取指定关键字的元数据值,会遍历继承链
    let result5 = Reflect.getMetadata(metadataKey, target);
    let result6 = Reflect.getMetadata(metadataKey, target, propertyKey);
    
    // 获取指定关键字的元数据值,只查找自己的,不会遍历继承链
    let result7 = Reflect.getOwnMetadata(metadataKey, target);
    let result8 = Reflect.getOwnMetadata(metadataKey, target, propertyKey);
    
    // 获取元数据的所有关键字,会遍历继承链
    let result9 = Reflect.getMetadataKeys(target);
    let result10 = Reflect.getMetadataKeys(target, propertyKey);
    
    // 获取元数据的所有关键字,只获取自己的,不会遍历继承链
    let result11 = Reflect.getOwnMetadataKeys(target);
    let result12 = Reflect.getOwnMetadataKeys(target, propertyKey);
    
    // 删除指定关键字的元数据
    let result13 = Reflect.deleteMetadata(metadataKey, target);
    let result14 = Reflect.deleteMetadata(metadataKey, target, propertyKey);
    
    // 装饰器方式设置元数据
    @Reflect.metadata(metadataKey, metadataValue)
    class C {
        @Reflect.metadata(metadataKey, metadataValue)
        method() {
        }
    }
    

    3. design类型元数据

    要使用design类型元数据需要在tsconfig.json中设置emitDecoratorMetadata为true,如下所示:

    • tsconfig.json
    {
      "compilerOptions": {
    …
        // 是否启用实验性的ES装饰器
        "experimentalDecorators": true
    
        // 是否自动设置design类型元数据(关键字有"design:type"、"design:paramtypes"、"design:returntype")
        "emitDecoratorMetadata": true
      }
    }
    

    emitDecoratorMetadata 设为true后,会自动设置design类型的元数据,通过以下方式可以获取元数据的值:

    let result1 = Reflect.getMetadata('design:type', target, propertyKey);
    let result2 = Reflect.getMetadata('design:paramtypes', target, propertyKey);
    let result3 = Reflect.getMetadata('design:returntype', target, propertyKey);
    

    不同类型的装饰器获得的 design 类型的元数据值,如下表所示:

    装饰器类型 design:type design:paramtypes design:returntype
    类装饰器 构造函数所有参数类型组成的数组
    属性装饰器 属性的类型
    方法装饰器 Function 方法所有参数的类型组成的数组 方法返回值的类型
    参数装饰器 所属方法所有参数的类型组成的数组

    示例代码:

    const MyClassDecorator: ClassDecorator = (target: any) => {
        const type = Reflect.getMetadata('design:type', target);
        console.log(`类[${target.name}] design:type = ${type && type.name}`);
    
        const paramTypes = Reflect.getMetadata('design:paramtypes', target);
        console.log(`类[${target.name}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
    
        const returnType = Reflect.getMetadata('design:returntype', target)
        console.log(`类[${target.name}] design:returntype = ${returnType && returnType.name}`);
    };
    
    const MyPropertyDecorator: PropertyDecorator = (target: any, key: string) => {
        const type = Reflect.getMetadata('design:type', target, key);
        console.log(`属性[${key}] design:type = ${type && type.name}`);
    
        const paramTypes = Reflect.getMetadata('design:paramtypes', target, key);
        console.log(`属性[${key}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
    
        const returnType = Reflect.getMetadata('design:returntype', target, key);
        console.log(`属性[${key}] design:returntype = ${returnType && returnType.name}`);
    };
    
    const MyMethodDecorator: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => {
        const type = Reflect.getMetadata('design:type', target, key);
        console.log(`方法[${key}] design:type = ${type && type.name}`);
    
        const paramTypes = Reflect.getMetadata('design:paramtypes', target, key);
        console.log(`方法[${key}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
    
        const returnType = Reflect.getMetadata('design:returntype', target, key)
        console.log(`方法[${key}] design:returntype = ${returnType && returnType.name}`);
    };
    
    const MyParameterDecorator: ParameterDecorator = (target: any, key: string, paramIndex: number) => {
        const type = Reflect.getMetadata('design:type', target, key);
        console.log(`参数[${key} - ${paramIndex}] design:type = ${type && type.name}`);
    
        const paramTypes = Reflect.getMetadata('design:paramtypes', target, key);
        console.log(`参数[${key} - ${paramIndex}] design:paramtypes =`, paramTypes && paramTypes.map(item => item.name));
    
        const returnType = Reflect.getMetadata('design:returntype', target, key)
        console.log(`参数[${key} - ${paramIndex}] design:returntype = ${returnType && returnType.name}`);
    };
    
    @MyClassDecorator
    class MyClass {
        @MyPropertyDecorator
        myProperty: string;
    
        constructor (myProperty: string) {
            this.myProperty = myProperty;
        }
    
        @MyMethodDecorator
        myMethod (@MyParameterDecorator index: number, name: string): string {
            return `${index} - ${name}`;
        }
    }
    
    

    输出结果如下:

    属性[myProperty] design:type = String
    属性[myProperty] design:paramtypes = undefined
    属性[myProperty] design:returntype = undefined
    参数[myMethod - 0] design:type = Function
    参数[myMethod - 0] design:paramtypes = [ 'Number', 'String' ]
    参数[myMethod - 0] design:returntype = String
    方法[myMethod] design:type = Function
    方法[myMethod] design:paramtypes = [ 'Number', 'String' ]
    方法[myMethod] design:returntype = String
    类[MyClass] design:type = undefined
    类[MyClass] design:paramtypes = [ 'String' ]
    类[MyClass] design:returntype = undefined
    

    四、 装饰器应用

    使用装饰器可以实现自动注册路由,通过给Controller层的类和方法添加装饰器来定义路由信息,当创建路由时扫描指定目录下所有Controller,获取装饰器定义的路由信息,从而实现自动添加路由。

    装饰器代码

    • src/common/decorator/controller.ts
    export interface Route {
        propertyKey: string,
        method: string;
        path: string;
    }
    
    export function Controller(path: string = ''): ClassDecorator {
        return (target: any) => {
            Reflect.defineMetadata('basePath', path, target);
        }
    }
    
    export type RouterDecoratorFactory = (path?: string) => MethodDecorator;
    
    export function createRouterDecorator(method: string): RouterDecoratorFactory {
        return (path?: string) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
            const route: Route = {
                propertyKey,
                method,
                path: path || ''
            };
            if (!Reflect.hasMetadata('routes', target)) {
                Reflect.defineMetadata('routes', [], target);
            }
            const routes = Reflect.getMetadata('routes', target);
            routes.push(route);
        }
    }
    
    export const Get: RouterDecoratorFactory = createRouterDecorator('get');
    export const Post: RouterDecoratorFactory = createRouterDecorator('post');
    export const Put: RouterDecoratorFactory = createRouterDecorator('put');
    export const Delete: RouterDecoratorFactory = createRouterDecorator('delete');
    export const Patch: RouterDecoratorFactory = createRouterDecorator('patch');
    

    控制器代码

    • src/controller/roleController.ts
    import Koa from 'koa';
    import { Controller, Get } from '../common/decorator/controller';
    import RoleService from '../service/roleService';
    
    @Controller()
    export default class RoleController {
    
        @Get('/roles')
        static async getRoles (ctx: Koa.Context) {
            const roles = await RoleService.findRoles();
            ctx.body = roles;
        }
    
        @Get('/roles/:id')
        static async getRoleById (ctx: Koa.Context) {
            const id = ctx.params.id;
            const role = await RoleService.findRoleById(id);
            ctx.body = role;
        }
    }
    
    • src/controller/userController.ts
    import Koa from 'koa';
    import { Controller, Get } from '../common/decorator/controller';
    import UserService from '../service/userService';
    
    @Controller('/users')
    export default class UserController {
        @Get()
        static async getUsers (ctx: Koa.Context) {
            const users = await UserService.findUsers();
            ctx.body = users;
        }
    
        @Get('/:id')
        static async getUserById (ctx: Koa.Context) {
            const id = ctx.params.id;
            const user = await UserService.findUserById(id);
            ctx.body = user;
        }
    }
    

    路由器代码

    • src/common /scanRouter.ts
    import fs from 'fs';
    import path from 'path';
    import KoaRouter from 'koa-router';
    import { Route } from './decorator/controller';
    
    // 扫描指定目录的Controller并添加路由
    function scanController(dirPath: string, router: KoaRouter): void {
        if (!fs.existsSync(dirPath)) {
            console.warn(`目录不存在!${dirPath}`);
            return;
        }
        const fileNames: string[] = fs.readdirSync(dirPath);
    
        for (const name of fileNames) {
            const curPath: string = path.join(dirPath, name);
            if (fs.statSync(curPath).isDirectory()) {
                scanController(curPath, router);
                continue;
            }
            if (!(/(.js|.jsx|.ts|.tsx)$/.test(name))) {
                continue;
            }
            try {
                const scannedModule = require(curPath);
                const controller = scannedModule.default || scannedModule;
                const isController: boolean = Reflect.hasMetadata('basePath', controller);
                const hasRoutes: boolean = Reflect.hasMetadata('routes', controller);
                if (isController && hasRoutes) {
                    const basePath: string = Reflect.getMetadata('basePath', controller);
                    const routes: Route[] = Reflect.getMetadata('routes', controller);
                    let curPath: string, curRouteHandler;
                    routes.forEach( (route: Route) => {
                        curPath = path.posix.join('/', basePath, route.path);
                        curRouteHandler = controller[route.propertyKey];
                        router[route.method](curPath, curRouteHandler);
                        console.info(`router: ${controller.name}.${route.propertyKey} [${route.method}] ${curPath}`)
                    })
                }
            } catch (error) {
                console.warn('文件读取失败!', curPath, error);
            }
    
        }
    }
    
    export default class ScanRouter extends KoaRouter {
        constructor(opt?: KoaRouter.IRouterOptions) {
            super(opt);
        }
    
        scan (scanDir: string | string[]) {
            if (typeof scanDir === 'string') {
                scanController(scanDir, this);
            } else if (scanDir instanceof Array) {
                scanDir.forEach(async (dir: string) => {
                    scanController(dir, this);
                });
            }
        }
    }
    

    创建路由代码

    • src/router.ts
    import path from 'path';
    import ScanRouter from './common/scanRouter';
    
    const router = new ScanRouter();
    
    router.scan([path.resolve(__dirname, './controller')]);
    
    export default router;
    

    五、 说明

    本文介绍了如何在node服务中使用装饰器,当需要增加某些额外的功能时,就可以不修改代码,简单地通过添加装饰器来实现功能。本文相关的代码已提交到GitHub以供参考,项目地址:https://github.com/liulinsp/node-server-decorator-demo

    作者:宜信技术学院 刘琳

  • 相关阅读:
    ubuntu下使用golang、qml与ubuntu sdk开发桌面应用 (简单示例)
    Go Revel 学习指南
    Go Revel
    Go Revel
    Go Revel
    Go Revel
    Go Revel
    Go Revel
    Go Revel
    Go Revel
  • 原文地址:https://www.cnblogs.com/yixinjishu/p/13920322.html
Copyright © 2011-2022 走看看