搭建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:00523

评论

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

Python处理视频文件的实用姿势

程一初

Python 自动化 办公

Python处理邮件和机器人的实用姿势

程一初

Python 自动化 办公

Axure导出为PDF

波菠菜

定时任务最简单的3种实现方法(超实用)

王磊

Java 定时任务

一文讲透布隆过滤器

flyer0126

布隆过滤器

Redis追命连环问,你能回答到第几问?(上)Redis简介,数据类型及缓存雪崩缓存击穿缓存穿透

大柚子

Java redis 缓存 面试 后端

Python处理图像文件的实用姿势

程一初

Python 自动化 办公

Python处理音频文件的实用姿势

程一初

Python 自动化 办公

Truncate用法详解

Simon

MySQL

朱嘉明 算力革命背后是分配制度革命 没有算力就没有未来

CECBC区块链专委会

区块链 数字货币 数字经济

从雕像到肖像画,这位设计师用 GAN 和 PS 复原了他眼中的古罗马皇帝「群像」

程序员生活志

Python处理PPT文件的实用姿势

程一初

Python 自动化 办公

深圳区块链支付系统开发,USDT支付系统服务商

13530558032

企业信息化到底重不重要?

代码制造者

低代码 零代码 信息化 编程开发 运营管理

基于ALBERT的文本相似度解决方案

华宇法律科技

人工智能 自然语言处理 Pytho

当地铁站都比你更努力

escray

面试 学习笔记 面试现场

高频面试题——你真的搞懂物理内存与虚拟内存了吗

大柚子

操作系统 内存管理 虚拟内存 物理内存

ARTS 打卡第二周(200518-200524)

老胡爱分享

ARTS 打卡计划

巧用SQL拼接语句

Simon

MySQL sql

非IT行业大企程序员讲述MIS系统开发案例

Philips

Java、 企业信息化 .net core 计算机程序设计艺术 企业开发

一个人的精益

escray

面试 学习笔记 面试现场

ARTS 打卡第三周(200525-200531)

老胡爱分享

ARTS 打卡计划

MySQL如何快速插入数据

Simon

MySQL 数据库

小米的护城河

石云升

小米 护城河

浅谈备受开发者好评的.NET core敏捷开发工具,讲讲LEARUN工作流引擎

Learun

工作流 开发工具 计算机程序设计艺术 表单

教你用SQL实现统计排名

Simon

MySQL

马方业:区块链就是新未来 区块链就是新财富

CECBC区块链专委会

区块链 新未来 新财富

业务架构是什么?

周金根

BIZBOK 业务架构 IT帮 周金根

区块链交易所系统开发内容,数字货币交易所搭建

13530558032

交易所合约跟单开发方,数字资产合约跟单系统搭建

13530558032

程序员不愿996,创建6个涉黄平台,涉案5000余万元!

程序员生活志

程序员

AI如何在普惠金融的探索中发挥作用?

AI如何在普惠金融的探索中发挥作用?

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