preface

Decorators extend the functionality of specified classes/class methods/attributes/parameters.

It is essentially a function that relies on Object.defineProperty on the underlying implementation.

Over time, decorators are no longer just syntactic candy to simplify code, but can also be used as a solution for certain scenarios (such as authentication).

Keeping decorators to a single responsibility, combined with flexibility, makes code cleaner and makes development more efficient.

The following uses a TS project as an example to introduce the basic usage and business scenarios of common types of decorators.

Project initialization

  1. Global Installation of TSnpm install -g typescript
  2. Create a folder and enter
  3. Generate package. Jsonnpm init -y
  4. Generate tsconfig. Jsontsc --init
  5. Ts and TS-Node are installed in the projectnpm i typescript ts-node
  6. Nodemon is installed in the projectnpm i nodemon
  7. Set experimentalDecorators to true in tsconfig and add decorator support
  8. Add the script start to package.json:nodemon -e ts --exec ts-node src/app
  9. Start the projectnpm start
  • Ts-node compiles ts to a JS file and executes it
  • The Nodemon automatically restarts after detecting that the target file is changed
  • Nodemon-e indicates that a supported file extension is added, and –exec indicates that a specified command is executed
  • If you are uncomfortable with implicit any, you can set noImplicitAny to false in tsconfig

The basic use

Class decorator

The class decorator applies to the specified class, and the target gets the constructor of the class.

You can do a lot of things with target, like add extra methods and attributes.


const logName: ClassDecorator = target= > {
  // Reflection API like target[prop]
  // Reflection will be used later
  // reflect-metadata can also be seen on NPM
  const name = Reflect.get(target, 'name');
  console.log(name);
};

@logName 
class User {}


Copy the code

Class method decorator

The class method decorator acts on a class method and takes three parameters: the class’s constructor (static method) or prototype object (instance method), the property name, and the description object of that property.

const check: MethodDecorator = (target, key, descriptor: PropertyDescriptor) = > {
 // Cache the old function, which is actually the target object of the decorator
 // This refers to the say method
 const fn = descriptor.value;
 // Override this method to customize some logic
 // The decoration function is preserved, so the old method is called at the end
  descriptor.value = function () {
    if(target.constructor.name ! = ='User') {
      console.error('method say must called by class User');
      return;
    }
    fn.call(this);
  };
 // Returns the property description object
  return descriptor;
};


class User {
  @check // Use the class method decorator
  say() {
    console.log('hi~'); }}class Cat {
  @check // Use the class method decorator
  say() {
    console.log('hi~'); }}new User().say();// hi~
new Cat().say();// method say must called by class User

Copy the code

The decorators used above do not accept arguments. If you need to receive parameters, wrap another layer of functions (using closures).


// Auth is a simple permission decorator that restricts a method to an administrator
const auth =
  (isAdmin = false) = >
    (target, key, descriptor: PropertyDescriptor) = > {
      const fn = descriptor.value;
      descriptor.value = function () {
        if(! isAdmin) {console.error('no auth');
          return;
        }
        fn.call(this);
      };

      return descriptor;
    };

class User {
  @auth(true) // the auth method call returns a class method decorator
  edit() {
    console.log('edit'); }}If the auth entry is true, print edit
// Instead, print no auth
new User().edit();

Copy the code

Multiple decorators can act on the same target object.


class UserCtrl {

  @auth(['admin'])/ / authentication
  @get('/api/user/list') // Set the request method
  listUser(){
    // xxx}}Copy the code

The business scenario

A classic scenario is the automatic loading of server-side routes with reflection and decorators.

The following takes a KOA project as an example to introduce the specific implementation of this part of the function.

Koa environment setup

  1. Install koA and KoA-Routernpm i koa koa-router
  2. Install koA-related type declarationsnpm i @types/koa @types/koa-router
  3. The root directory SRC /app.js writes the following code

import Koa from 'koa';
import Router from 'koa-router';

const app = new Koa();
const router = new Router();
router.get('/'.ctx= > {
  ctx.body = 'hello';
});
app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000.() = > {
  console.log('run server... ');
});

Copy the code
  1. Start the projectnpm start
  2. You can test it by opening http://localhost:3000 port in your browser

The desired effect

The above code has not been optimized, and the business logic is all coupled together.

We need a clearer, more maintainable way of organizing code.

It goes down like this:


@controller('/main')
export default class MainCtrl {
  @get('/index')
  async index(ctx) {
    ctx.body = 'hello world';
  }
  @get('/home')
  async home(ctx) {
    ctx.body = 'hello home'; }}// We want the above code to be equivalent to the following

router.get('/main/index'.ctx= >{
   ctx.body = 'hello world';
})
router.get('/main/home'.ctx= >{
   ctx.body = 'hello home';
})
Copy the code

Implementation approach

Our ultimate goal is to spell out the routing information contained in the Controller and complete the registration, which is actually a data set and get process.

The request prefix and routing information are set on the prototype object of the corresponding controller through the decorator.

All controllers are then iterated and instantiated, and the data stored on the prototype is retrieved by reflection to complete route registration.

A decorator decorator. Ts

export const controller =
  (prefix = ' ') :ClassDecorator= >
    (target: any) = > {
      target.prototype.prefix = prefix;
    };

type Method = 'get' | 'post' | 'delete' | 'options' | 'put' | 'head';

export interface RouteDefinition {
  path: string;
  requestMethod: Method;
  methodName: string;
}

const creatorFactory =
  (requestMethod: Method) = >
    (path: string) :MethodDecorator= >
      (target, name) = > {
        if (!Reflect.has(target.constructor, 'routes')) {
          Reflect.defineProperty(target.constructor, 'routes', {
            value: [],}); }const routes = Reflect.get(target.constructor, 'routes');
        routes.push({
          requestMethod,
          path,
          methodName: name,
        });
      };

export const get = creatorFactory('get');
// export const post = creatorFactory('post');
// export const del = creatorFactory('delete');
// export const put = creatorFactory('put');
// export const options = creatorFactory('options');
// export const head = creatorFactory('head');

Copy the code

Register route app.ts

import Koa from 'koa';
import Router from 'koa-router';
import MainCtrl from './main-ctrl';
const app = new Koa();
const router = new Router();
router.get('/'.ctx= > {
  ctx.body = 'hello';
});

[MainCtrl].forEach(controller= > {
  const instance: any = new controller();
  const { prefix } = instance;
  const routes = Reflect.get(controller, 'routes');
  routes.forEach(route= > {
    router[route.requestMethod](prefix + route.path, ctx= > {
      instance[route.methodName](ctx);
    });
  });
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000.() = > {
  console.log('run server... ');
});


Copy the code

Controller main – CTRL. Ts

import { controller, get } from './decorator';
@controller('/main')
export default class MainCtrl {
  @get('/index')
  async index(ctx) {
    ctx.body = 'hello world';
  }
  @get('/home')
  async home(ctx) {
    ctx.body = 'hello home'; }}Copy the code

Automatic scanning

Another drawback of the above implementation is that if there are too many controllers, you need to import them manually every time, which is very troublesome.

A more elegant approach is to batch scan, using glob to scan a directory of specified controllers (such as controllers).

  1. Install the globnpm i glob
  2. Root directory new load. Ts, write the following code
import * as glob from 'glob';
import path from 'path';
export default (folder: string.router: any) = > {// Scan all ts files in the specified folder
  glob.sync(path.join(folder, '**/*.ts')).forEach(item= > {
    / / load controller
    const controller = require(item).default;
    / / instantiate
    const instance: any = new controller();
    const { prefix } = instance;
    const routes = Reflect.get(controller, 'routes');
    routes.forEach(route= > {
      router[route.requestMethod](prefix + route.path, ctx= > {
        instance[route.methodName](ctx);
      });
    });
  });
};
Copy the code
  1. Modify the app. Ts
import Koa from 'koa';
import Router from 'koa-router';
import path from 'path';
import load from './load';
const app = new Koa();
const router = new Router();
router.get('/'.ctx= > {
  ctx.body = 'hello';
});
load(path.resolve(__dirname, './controllers'), router);
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000.() = > {
  console.log('run server... ');
});
Copy the code
  1. Move the previous main-Ctrl. ts to the new controllers directory
  2. Browser access to the corresponding path, test

The source address

decorator

farewell

Love is fickle,

Is a move that hurt.

Thank you so much for reading my article,

I’m Cold Moon Heart. See you next time.