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. 執行順序
不同聲明上的裝飾器將按以下順序執行:
- 實例成員的裝飾器:
參數裝飾器 > 方法裝飾器 > 訪問符裝飾器/屬性裝飾器 - 靜態成員的裝飾器:
參數裝飾器 > 方法裝飾器 > 訪問符裝飾器/屬性裝飾器 - 構造函數的參數裝飾器
- 類裝飾器
如果同一個聲明有多個裝飾器,離聲明越近的裝飾器越早執行:
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。
作者:宜信技術學院 劉琳