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

2020 年 11 月 22 日

搭建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 = (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 = (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.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. 执行顺序


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


  • 实例成员的装饰器: 参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器

  • 静态成员的装饰器: 参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器

  • 构造函数的参数装饰器

  • 类装饰器


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


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


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


作者介绍


宜信技术学院 刘琳


本文转载自宜信技术学院。


原文链接


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


2020 年 11 月 22 日 14:001065

评论

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

推动敏捷,就是推动软件业变革

盛安德软件

敏捷 推动软件业变革

JavaScript 基础拾遗(一)

吴昊泉

Java 学习 文章收集

Django的ListView超详细用法(含分页paginate功能)

Young先生

Python django ListView 分页

SpringBoot瘦身

JFound

Spring Boot sprnig

竟然有人想看我的「日记」,满足一下大家

非著名程序员

学习 程序人生 提升认知

Android | Tangram动态页面之路(五)Tangram原理

哈利迪

android

Elastic Stack 系列专辑

Yezhiwei

elasticsearch Logstash Kibana ELK Elastic Stack

Spring Security 如何将用户数据存入数据库?

江南一点雨

Java spring Spring Cloud Spring Boot spring security

2020年全球经济萎缩,火花国际PLUS逆袭而来闪耀数字经济

极客编

企业数字化转型:用 SpreadJS 打造互通互链的电力系统物联网

Geek_Willie

数字化转型 SpreadJS 电力

ARTS_20200520

凌轩

Java ARTS 打卡计划

万字长文带你看懂Mybatis缓存机制

程序员小岑

Java 源码 技术 mybatis

Java 25周年:MovedByJava之观点

范学雷

Java 架构 编程语言

关于架构的几件小事:System context

北风

系统架构 系统性思考 架构师 系统上下文 极客大学架构师训练营

往日之歌

彭宏豪95

如何做好 To B 的 SAAS 服务

路边水果摊

SASS 企业 服务

敏捷为什么会失败之「PA-SA-WAKA-DA」理论

Worktile

Scrum 敏捷开发 Agile

职场“潜”规则

俊毅

个人成长 职场 新人 人才培养 能力模型

程序员需要了解的硬核知识大全

cxuan

Java c 计算机基础

深入剖析ThreadLocal原理

JFound

Java

当我们持续感觉很糟糕要怎么办

七镜花园-董一凡

写作 生活质量 情感

JVM源码分析之synchronized实现

猿灯塔

SQLite是什么

这小胖猫

sqlite 数据库 RDBMS 存储

为提升网点业务员效率,我们做的事情。

黄大路

商业

nginx 概念及上手

HelloZyjS

我的编程之路-4(进阶)

顿晓

进阶 看书 编程之路

Enhanced Github:一个 GitHub 专用的好插件

非著名程序员

GitHub 程序员 效率工具

kotlin 200行代码开发一个简化版Guice

陈吉米

Java kotlin guice ioc mynlp

Redis6.0 多线程源码分析

代码诗人

redis 源码 技术 线程模型

Redis 命令执行过程(下)

程序员历小冰

redis 源码分析

回“疫”录(22):我以为结束了,其实才开始

小天同学

疫情 回忆录 现实纪录 纪实

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