29

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

 3 years ago
source link: http://college.creditease.cn/detail/392
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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. 执行顺序

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

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

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

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

  4. 类装饰器

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

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

作者:宜信技术学院 刘琳


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK