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 = (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 = (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => TypedPropertyDescriptor | 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。
作者:宜信技术学院 刘琳