搭建node服务(四):Decorator装饰器

2020 年 11 月 13 日

搭建node服务(四):Decorator装饰器

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;};
@Controllerclass 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); // 输出结果为:trueconsole.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()); // 输出:fun1console.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.133Zentity.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. 执行顺序


不同声明上的装饰器将按以下顺序执行:


  1. 实例成员的装饰器:参数装饰器 &gt; 方法装饰器 &gt; 访问符装饰器/属性装饰器

  2. 静态成员的装饰器:参数装饰器 &gt; 方法装饰器 &gt; 访问符装饰器/属性装饰器

  3. 构造函数的参数装饰器

  4. 类装饰器


如果同一个声明有多个装饰器,离声明越近的装饰器越早执行:


const A: ClassDecorator = (target) => {    console.log('A');};
const B: ClassDecorator = (target) => { console.log('B');};
@A@Bclass MyClass {
}
/* 输出结果:BA*/
复制代码


三、 Reflect Metadata


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}`);};
@MyClassDecoratorclass 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


本文转载自公众号宜信技术学院(ID:CE_TECH)。


原文链接


搭建node服务(四):Decorator装饰器


2020 年 11 月 13 日 10:00555

评论

发布
暂无评论
发现更多内容

最初的梦想

小天同学

写作 成长 梦想

架构师训练营第 0 期 - 第 2 周 - 学习总结

极客大学架构师训练营

第二周作业

CP

第二周学习总结

CP

架构师训练营第二周感悟

路人

第二周作业

晓雷

架构师训练营——设计模式篇_作业

独孤魂

作业

架构师训练营 - 第二周 - 命题作业

Anrika

架构师 极客大学架构师训练营

依赖倒置原则 & 类图优化

lei Shi

接口隔离原则设计缓存Cache工具类

王鹏飞

架构师训练营 第二周 学习总结

极客

架构师系列之面向对象即设计原则

彭阿三

架构

不要再用面向对象语言编写面向过程的代码了

鸠摩智

Python中的下划线

shiziwen

Python

【架构训练营】第二期

云064

第二周总结

Young

依赖倒置(DIP)

Lane

极客大学架构师训练营

week2作业

雪涛公子

第2周作业

sunpengjian

因为知道了30+款在线工具,我的工作效率提升500%!

Hollis

数据库大咖讲坛活动6月18日墨天轮平台线上举行,阿里腾讯达梦众多数据库大咖齐聚!

墨天轮

数据库 腾讯云 阿里云 数据库设计

架构师训练营第 02 周——总结

李伟

极客大学架构师训练营

Week02 作业

极客大学架构师训练营

不懂什么是锁?看看这篇你就明白了

cxuan

Java 并发

如何理解依赖倒置

丿淡忘

极客大学架构师训练营 依赖倒置原则

OOD四大原则

金桔🍊

实现自己架构的主要手段

重新来过

第二周作业:设计原则

Larry

依赖倒置原则

Young

架构师训练营 - 第二周 - 学习总结

Anrika

架构师 极客大学架构师训练营

架构师训练营 - 学习总结 - 第二讲

吕浩

搭建node服务(四):Decorator装饰器-InfoQ