关于node.js:搭建node服务四Decorator装饰器

51次阅读

共计 15668 个字符,预计需要花费 40 分钟才能阅读完成。

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 += `\n * 第 ${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. 实例成员的装璜器:

参数装璜器 > 办法装璜器 > 拜访符装璜器 / 属性装璜器

  1. 动态成员的装璜器:

参数装璜器 > 办法装璜器 > 拜访符装璜器 / 属性装璜器

  1. 构造函数的参数装璜器
  2. 类装璜器

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

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。

作者:宜信技术学院 刘琳

正文完
 0