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. 执行程序
不同申明上的装璜器将按以下程序执行:
- 实例成员的装璜器:
参数装璜器 > 办法装璜器 > 拜访符装璜器 / 属性装璜器
- 动态成员的装璜器:
参数装璜器 > 办法装璜器 > 拜访符装璜器 / 属性装璜器
- 构造函数的参数装璜器
- 类装璜器
如果同一个申明有多个装璜器,离申明越近的装璜器越早执行:
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。
作者:宜信技术学院 刘琳