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
- Global Installation of TS
npm install -g typescript
- Create a folder and enter
- Generate package. Json
npm init -y
- Generate tsconfig. Json
tsc --init
- Ts and TS-Node are installed in the project
npm i typescript ts-node
- Nodemon is installed in the project
npm i nodemon
- Set experimentalDecorators to true in tsconfig and add decorator support
- Add the script start to package.json:
nodemon -e ts --exec ts-node src/app
- Start the project
npm 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
- Install koA and KoA-Router
npm i koa koa-router
- Install koA-related type declarations
npm i @types/koa @types/koa-router
- 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
- Start the project
npm start
- 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).
- Install the glob
npm i glob
- 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
- 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
- Move the previous main-Ctrl. ts to the new controllers directory
- 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.