Build node service (4): Decorator

Build node service (4): Decorator

Decorator (decorator) is a class-related grammar in ECMAScript, which is used to dynamically add functions to objects during runtime. Node.js does not yet support Decorator, you can use Babel for conversion, or you can use Decorator in TypeScript. This example is based on TypeScript to introduce how to use Decorator in the node service.

One, TypeScript related

Because TypeScript is used, TypeScript-related dependencies need to be installed and the tsconfig.json configuration file is added to the root directory, which will not be described in detail here. To use Decorator in TypeScript, you must set experimentalDecorators in tsconfig.json to true, as shown below:

tsconfig.json

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

Second, the decorator introduction

1. Simple example

Decorator is actually a kind of syntactic sugar. Here is a simple example of decorator written in TypeScript:


const Controller: ClassDecorator = (target: any) => {
    target.isController = true;
};

@Controller
class MyClass {

}

console.log(MyClass.isController); // 输出结果:true

Controller is a class decorator. Use the decorator in the form of @Controller before the declaration of the MyClass class. After adding the decorator, the value of MyClass. isController is true.
The compiled code is as follows:

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. Factory Method

When using a decorator, it is sometimes necessary to pass some parameters to the decorator. At this time, you can use the decorator factory method. Examples are as follows:

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"

The controller method is a decorator factory method. After execution, it returns a class decorator. By adding a decorator in the format of @controller('My') above the MyClass class, the value of MyClass.isController after adding is true, and the value of MyClass.controllerLabel Is "My".

3. Class decorator

The type of class decorator is defined as follows:

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

The class decorator has only one parameter target, which is the constructor of the class.
The return value of the class decorator can be empty, or it can be a new constructor.
Here is an example of a class decorator:

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 is a class decorator factory, which is added before the class declaration in the format of @mixin() when used. The function is to add the methods of the objects in the parameter array to the prototype object of MyClass.

4. Attribute Decorator

The type of attribute decorator is defined as follows:

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

The property decorator has two parameters target and propertyKey.

  • target: The static property is the constructor of the class, and the instance property is the prototype object of the class
  • propertyKey: property name

Here is an example of an attribute decorator:

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 is an attribute decorator. It is added before the attribute declaration when it is used. Its function is to add the required rules of the corresponding attribute in the target's custom attribute metadata. After adding the decorator in the above example , the value of target. metadata is: {name: {required: true }, type: {required: true} }.
By reading __metadata, you can obtain the required attributes to be set, so as to verify the instance object. The relevant code for verification is as follows:

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. Method decorator

The type of method decorator is defined as follows:

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

The method decorator has 3 parameters target, propertyKey and descriptor.

  • target: The static method is the constructor of the class, and the instance method is the prototype object of the class
  • propertyKey: method name
  • descriptor:
    The return value of the attribute descriptor method decorator can be empty or it can be a new attribute descriptor.
    Here is an example of a method decorator:
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 is a method decorator, which is added before the method declaration when used to automatically output the method call log. The third parameter of the method decorator is the attribute descriptor. The value of the attribute descriptor represents the execution function of the method. The original value is replaced with a new function. The new method will also call the original method, but output before calling the original method. Posted a log.

6. Accessor decorator

The use of the accessor decorator is the same as that of the method decorator. The parameters and return values ​​are the same, except that the accessor decorator is used before the accessor declaration. It should be noted that TypeScript does not allow both get and set accessors to decorate a member at the same time. The following is an example of an accessor decorator:


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
 */

In the MyClass class, there is an attribute createDate of Date type, and a createTime method with get declaration is added to obtain the millisecond value of createDate by entity.createTime. But createTime is not enumerable by default, and createTime can be an enumerable property by adding the @Enumerable decorator before the declaration.

7. Parameter decorator

The type of parameter decorator is defined as follows:

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

The parameter decorator has 3 parameters target, propertyKey and descriptor.

  • target: The parameter of the static method is the constructor of the class, and the parameter of the instance method is the prototype object of the class
  • propertyKey: the method name of the method where the parameter is located
  • parameterIndex: The index value in the method parameter list is based
    on the @Log method decorator example above, and then the parameter decorator is used to expand the log adding function and increase the log output of parameter information. The code is as follows:
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 is a parameter decorator, which is added before the parameter declaration when used to output parameter information logs.

8. Order of execution

Decorators on different declarations will be executed in the following order:

  1. Decorator for instance members:
    parameter decorator> method decorator> accessor decorator/attribute decorator
  2. Decorator for static members:
    parameter decorator> method decorator> accessor decorator/attribute decorator
  3. Constructor parameter decorator
  4. Class decorator

If there are multiple decorators for the same declaration, the decorator closer to the declaration is executed earlier:

const A: ClassDecorator = (target) => {
    console.log('A');
};

const B: ClassDecorator = (target) => {
    console.log('B');
};

@A
@B
class MyClass {

}

/* 输出结果:
B
A
*/

Three, Reflect Metadata

1. Installation dependencies

Reflect Metadata is an experimental interface, you can add some custom information to the class through decorators. This interface is not currently a part of the ECMAScript standard and requires the reflect-metadata gasket to be installed.

npm install reflect-metadata --save

or

yarn add reflect-metadata

In addition, you also need to import this module in a global location, such as the entry file.

import 'reflect-metadata';

2. Related interfaces

The interfaces provided by Reflect Metadata are as follows:

// 定义元数据
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 type metadata

To use design type metadata, you need to set emitDecoratorMetadata to true in tsconfig.json, as shown below:

  • tsconfig.json
{
  "compilerOptions": {
    // 是否启用实验性的ES装饰器
    "experimentalDecorators": true

    // 是否自动设置design类型元数据(关键字有"design:type"、"design:paramtypes"、"design:returntype")
    "emitDecoratorMetadata": true
  }
}

After emitDecoratorMetadata is set to true, the metadata of the design type will be automatically set, and the value of the metadata can be obtained in the following ways:

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

The metadata values ​​of design type obtained by different types of decorators are shown in the following table:

Decorator type design:type design:paramtypes design:returntype
Class decorator An array of all parameter types of the constructor
Attribute decorator Attribute type
Method decorator Function An array of the types of all parameters of the method Type of method return value
Parameter decorator An array of the types of all the parameters of the method

Sample code:

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}`;
    }
}

The output is as follows:

属性[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

Four, decorator application

Using decorators can realize automatic registration of routes. You can define routing information by adding decorators to the classes and methods of the Controller layer. When creating routes, scan all Controllers in the specified directory to obtain routing information defined by the decorators to automatically add routes.

Decorator code

  • 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');

Controller code

  • 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;
    }
}

Router code

  • 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);
            });
        }
    }
}

Create routing code

  • 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;

Five, description

This article introduces how to use decorators in node services. When you need to add some additional functions, you can simply add decorators to realize the functions without modifying the code. The code related to this article has been submitted to GitHub for reference, project address:https://github.com/liulinsp/node-server-decorator-demo

Author: Liu Lin Yixin Institute of Technology

Guess you like

Origin blog.51cto.com/14159827/2546467