Decorators are a class-related syntax in ECMAScript used to dynamically add functionality to objects at runtime. Decorators are not yet supported in Node.js and can be converted using Babel or in TypeScript. This example shows how to use a Decorator in a Node service based on TypeScript.

TypeScript related

Because you are using TypeScript, you need to install typescript-related dependencies and add the tsconfig.json configuration file to the root directory, which is not detailed here. To use Decorator decorators in TypeScript, you must set experimentalDecorators to true in tsconfig.json, as shown below:

tsconfig.json

{" compilerOptions ": {... // Whether to enable experimental ES decorator "experimentalDecorators": true}}Copy the code

Two, the introduction of decoration

1. Simple example

A Decorator is really a syntactic sugar. Here is a simple example of a Decorator written in TypeScript:

const Controller: ClassDecorator = (target: any) => { target.isController = true; }; @Controller class MyClass { } console.log(MyClass.isController); // Output result: trueCopy the code

Controller is a class decorator that is used in the form of @controller before the MyClass declaration. After adding the decorator, myClass. isController has the value true. The compiled code looks like this:

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);
Copy the code

2. Factory method

The decorator factory method can be used as an example:

function controller ( label: string): ClassDecorator { return (target: any) => { target.isController = true; target.controllerLabel = label; }; } @controller('My') class MyClass { } console.log(MyClass.isController); // The output is true console.log(myclass.controllerLabel); // Output: "My"Copy the code

The Controller method is a decorator factory method that returns a class decorator by adding a decorator at the top of the MyClass class in @ Controller (‘My’) format with the value true for myClass.isController. And the value of myClass.controllerLabel is “My”.

3. Class decorator

Class decorators are defined as follows:

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

The class decorator takes only one parameter, target, which is the constructor of the class. The return value of a class decorator can be empty or 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()); // Output: fun1 console.log(new MyClass().fun2()); // Output: fun2Copy the code

Mixin is a class decorator factory that is used in the @mixin() format before a class declaration to add the methods of the objects in the parameter array to the prototype object of MyClass.

4. Property decorator

The property decorator type is defined as follows:

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

The property decorator takes two parameters, target and propertyKey.

  • Target: Static properties are class constructors, instance properties are class prototype objects
  • PropertyKey: indicates the property name

Here is an example property 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;
}
Copy the code

@required is an attribute decorator that is added before a property declaration to add mandatory rules to target’s custom __metadata attribute. The target.__metadata value is {name: {required: true}, type: {required: true}}. By reading __metadata, you can obtain the required properties set to verify the instance object. The 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); // Output result: falseCopy the code

Method decorators

The method decorator type is defined as follows:

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

The method decorator takes three parameters: Target, propertyKey, and Descriptor.

  • Target: Static methods are class constructors, instance methods are class prototype objects
  • PropertyKey: method name
  • Descriptor: attribute descriptor

The return value of the method decorator can be empty, or it can be a new property 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) {the console. The log (` call ${className}. ${key} ` () method). 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(); // Output: call myclass.getName ()Copy the code

@log is a method decorator that is added to a method declaration when used to automatically print a method call Log. The third argument to the method decorator is the property descriptor. The value of the property descriptor represents the method’s execution function. The value of the property descriptor is replaced by a new function that calls the original method, but prints a log before calling the original method.

6. Accessor decorator

Accessor decorators are used in the same way as method decorators, with the same arguments and return values, except that accessor decorators precede accessor declarations. Note that TypeScript does not allow you to decorate both get and SET accessors for a member. Here is an example 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]); } /* Output: entity.createdate = 2020-04-08T10:40:51.133z entity.createTime = 1586342451133 */Copy the code

CreateTime (createTime); createTime (createTime); createTime (createTime); createTime (createTime); But createTime is not Enumerable by default. You can make createTime an Enumerable property by adding the @Enumerable decorator before the declaration.

7. Parameter decorator

The parameter decorator type is defined as follows:

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

Parameter decorators have three parameters: Target, propertyKey, and Descriptor.

  • Target: Static methods whose arguments are class constructors and instance methods whose arguments are class prototype objects
  • PropertyKey: method name of the method where the parameter is located
  • ParameterIndex: indicates the index value in the method parameter list

On the basis of the above example of @log method decorator, use the parameter decorator to extend the function of adding Log, 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.paramName} = ${params[item.paramIndex]} '; })} the console. The log (` call ${className}. ${key} () method ` + 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(); // Output: call myclass.getName () entity.setName('Jone Brown'); /* Output: call myClass.setNames () * the value of the 0th argument is: Jone Brown */ entity.setnames ('Jone', 'Brown'); /* Output: call myclass.setNames () * the value of the first argument lastName is: Brown * the value of the 0th argument firstName is: Jone */Copy the code

@logParam is a parameter decorator that is added to a parameter declaration when used to output a log of parameter information.

8. Execution sequence

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

  1. Decorators for instance members:

Parameter Decorator > Method Decorator > Accessor Decorator/Property Decorator 2. Static member decorators: Parameter Decorator > Method Decorator > Accessor Decorator/property decorator 3. Constructor parameter decorator 4. Class decorator

If the same declaration has more than one decorator, the closer the decorator to the declaration, the earlier it executes:

const A: ClassDecorator = (target) => { console.log('A'); }; const B: ClassDecorator = (target) => { console.log('B'); }; @a@b class MyClass {} /* Output result: B A */Copy the code

Third, Reflect the Metadata

1. Install dependencies

Reflect Metadata is an experimental interface that allows you to add custom information to classes via decorators. This interface is not currently part of the ECMAScript standard and requires reflect-Metadata shippers to be installed.

npm install reflect-metadata --save
Copy the code

or

yarn add reflect-metadata
Copy the code

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

import 'reflect-metadata';
Copy the code

2. Related interfaces

Reflect Metadata provides the following interfaces:

// defineMetadata reflect.definemetadata (metadataKey, metadataValue, target); Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey); // Let result1 = reflect.hasmetadata (metadataKey, target); let result2 = Reflect.hasMetadata(metadataKey, target, propertyKey); Result3 = reflect.hasownMetadata (metadataKey, target); let result4 = Reflect.hasOwnMetadata(metadataKey, target, propertyKey); Result5 = reflect.getMetadata (metadataKey, target); // getMetadata(metadataKey, target); let result6 = Reflect.getMetadata(metadataKey, target, propertyKey); Let result7 = Reflect. GetOwnMetadata (metadataKey, target); // Reflect. let result8 = Reflect.getOwnMetadata(metadataKey, target, propertyKey); Result9 = reflect.getMetadatakeys (target); let result10 = Reflect.getMetadataKeys(target, propertyKey); / / get all the keywords of metadata, the only access to their own, will not traverse the inheritance chain let result11 = Reflect. GetOwnMetadataKeys (target); let result12 = Reflect.getOwnMetadataKeys(target, propertyKey); Result13 = reflect.deletemetadata (metadataKey, target); let result14 = Reflect.deleteMetadata(metadataKey, target, propertyKey); @reflect. metadata(metadataKey, metadataValue) class C {@reflect. metadata(metadataKey, metadataValue) class C {@reflect. metadata(metadataKey, metadataValue) metadataValue) method() { } }Copy the code

3. Metadata of type design

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

  • tsconfig.json
{" compilerOptions ": {... // Whether to enable the experimental ES decorator "experimentalDecorators": True // Whether to automatically set the design type metadata (keyword: "design:type", "design:paramtypes", "design: returnType ") "emitDecoratorMetadata": true}}Copy the code

When emitDecoratorMetadata is set to true, the metadata of type Design is automatically set. You can obtain the value of the metadata as follows:

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

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

Decorator type design:type design:paramtypes design:returntype
Class decorator Constructor an array of all argument types
Attribute decorator Type of property
Method decorator Function Method is an array of the types of all the arguments to the The type of the value returned by the method
Parameter decorator An array of the types of all the arguments to the owning method
Sample code:
const MyClassDecorator: ClassDecorator = (target: any) => { const type = Reflect.getMetadata('design:type', target); Console. log(' class [${target.name}] design:type = ${type && type.name} '); const paramTypes = Reflect.getMetadata('design:paramtypes', target); Console. log(' class [${target.name}] Design: Paramtypes = ', Paramtypes && Paramtypes. Map (item => item.name)); const returnType = Reflect.getMetadata('design:returntype', Target) console.log(' class [${target.name}] Design: returnType = ${returnType && returnType.name} '); }; const MyPropertyDecorator: PropertyDecorator = (target: any, key: string) => { const type = Reflect.getMetadata('design:type', target, key); The console. The log (` properties [${key}] design: type = ${type && type. The name} `); const paramTypes = Reflect.getMetadata('design:paramtypes', target, key); Console. log(' property [${key}] Design: Paramtypes = ', paramtypes && Paramtypes. Map (item => item.name)); const returnType = Reflect.getMetadata('design:returntype', target, key); Console. log(' property [${key}] design: returnType = ${returnType && returnType.name} '); }; const MyMethodDecorator: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => { const type = Reflect.getMetadata('design:type', target, key); The console. The log (` method [${key}] design: type = ${type && type. The name} `); const paramTypes = Reflect.getMetadata('design:paramtypes', target, key); Console. log(' method [${key}] Design: Paramtypes = ', paramtypes && Paramtypes. Map (item => item.name)); const returnType = Reflect.getMetadata('design:returntype', target, Key) console.log(' method [${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(' parameter [${key} - ${paramIndex}] design:type = ${type && type.name} '); const paramTypes = Reflect.getMetadata('design:paramtypes', target, key); Console. log(' parameter [${key} - ${paramIndex}] Design: Paramtypes = ', Paramtypes && Paramtypes. Map (item => item.name)); const returnType = Reflect.getMetadata('design:returntype', target, Key) console.log(' parameter [${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}`; }}Copy the code

The following output is displayed:

Attribute [myProperty] Design :type = String Attribute [myProperty] Design: ParamTypes = Undefined attribute [myProperty] Design: ReturnType = Undefined parameter [mymethod-0] design:type = Function Parameter [mymethod-0] design:type = ['Number', Parameter [mymethod-0] Design: returnType = String method [myMethod] Design :type = Function method [myMethod] design:paramtypes = [ 'Number', [MyClass] design:type = undefined class [MyClass] Design :paramtypes = [ [MyClass] Design: returnType = undefinedCopy the code

Four, decorator application

Decorators can be used to automatically register routes. Route information is defined by adding decorators to classes and methods of the Controller layer. When creating a route, all controllers in the specified directory are scanned to obtain the route information defined by decorators, so as 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');Copy the code

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; }}Copy the code
  • 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; }}Copy the code

Router code

  • src/common /scanRouter.ts
import fs from 'fs'; import path from 'path'; import KoaRouter from 'koa-router'; import { Route } from './decorator/controller'; Function scanController(dirPath: string, router: KoaRouter): void {if (! Fs.existssync (dirPath)) {console.warn(' directory does not exist! ${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(' File read failed! ', 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); }); }}}Copy the code

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;
Copy the code

Five, description,

This article has shown you how to use decorators in node services. When you need to add some additional functionality, you can simply add decorators without changing the code. The code for this article has been submitted to GitHub for your reference. The project address is github.com/liulinsp/no… .

Author: Liu Lin, Creditease Institute of Technology