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": {
…
"experimentalDecorators": true
}
}
复制代码
二、装饰器介绍
1. 简单示例
Decorator 实际是一种语法糖,下面是一个简单的用 TypeScript 编写的装饰器示例:
const Controller: ClassDecorator = (target: any) => {
target.isController = true;
};
@Controller
class MyClass {
}
console.log(MyClass.isController);
复制代码
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);
console.log(MyClass.controllerLabel);
复制代码
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());
console.log(new MyClass().fun2());
复制代码
mixin 是一个类装饰器工厂,使用时以 @mixin() 格式添加到类声明前,作用是将参数数组中对象的方法添加到 MyClass 的原型对象上。
4. 属性装饰器
属性装饰器的类型定义如下:
type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
复制代码
属性装饰器有两个参数 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 {
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);
复制代码
5. 方法装饰器
方法装饰器的类型定义如下:
type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
复制代码
方法装饰器有 3 个参数 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();
复制代码
@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。
在上面 @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();
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
Reflect Metadata 是的一个实验性接口,可以通过装饰器来给类添加一些自定义的信息。这个接口目前还不是 ECMAScript 标准的一部分,需要安装 reflect-metadata 垫片才能使用。
npm install reflect-metadata
复制代码
或者
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,如下所示:
{
"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,获取装饰器定义的路由信息,从而实现自动添加路由。
装饰器代码
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');
复制代码
控制器代码
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;
}
}
复制代码
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;
}
}
复制代码
路由器代码
import fs from 'fs';
import path from 'path';
import KoaRouter from 'koa-router';
import { Route } from './decorator/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);
});
}
}
}
复制代码
创建路由代码
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装饰器
评论