装饰器(Decorators)是一种特殊的声明,可附加在类、方法、访问器、属性、参数声明上。
装饰器(Decorators)可以通过非侵入方式增强类、方法、访问器、属性、参数的能力。可以将业务逻辑代码和特殊能力和服务的代码分割开来,保证代码的灵活性和扩展性,合理的利用装饰器可以极大的提高我们的开发效率。
注意: 同样的滥用装饰器也会使代码本身逻辑变得扑朔迷离,如果确定一段代码不会在其他地方用到,或者一个函数的核心逻辑就是这些代码,那么就没有必要将它取出来作为一个装饰器来存在。
为什么需要装饰器
例如: 我们想记录一个方法执行的耗时
没有使用装饰器方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Model1 { getData() { let start = new Date().valueOf() try { return [{ id: 1, name: 'Niko' }, { id: 2, name: 'Bellic' }] } finally { let end = new Date().valueOf() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } }
console.log(new Model1().getData())
console.log(Model1.prototype.getData())
|
在上面代码中,getData
方法中包含了一些非业务的逻辑代码。而这些代码还是不可复用的。
使用装饰器的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| function log(target, descriptor, descriptor) { Object.defineProperty(target, key, { ...descriptor, value: function (...arg) { let start = new Date().valueOf() try { return descriptor.value.apply(this, arg) } finally { let end = new Date().valueOf() console.log(`start: ${start} end: ${end} consume: ${end - start}`) } } }) }
class Model1 { @log getData() { return [{ id: 1, name: 'Niko' }, { id: 2, name: 'Bellic' }] } }
|
上面的代码中,我们将增加了一个log
用来记录方法执行的时间。通过装饰器我们将业务逻辑和非业务逻辑代码分割开来。
装饰器函数执行时机
装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。
- 创建类和创建函数时立即执行装饰器
- 如果装饰器函数返回一个function,这个function会在所有的装饰器函数运行完毕后,继续运行
如何启用
目前 装饰器(Decorators)处于ECMAscript提案第二阶段(Stage2 Draft)。目前Typescript已经支持,只需要在tsconfig.json
做一下简单配置
1 2 3 4 5 6
| { "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
|
装饰器种类
Typescript中装饰器有五种类型:
装饰器/属性 |
类 装饰器 |
方法 装饰器 |
访问器 装饰器 |
属性 装饰器 |
参数 装饰器 |
位置 |
@foo class Bar {} |
@foo public bar() {} |
@foo get bar() |
@foo() bar: number |
bar(@foo para: string) {} |
传入参数 |
constructor |
target, propertyKey, descriptor |
target, propertyKey, descriptor |
target, propertyKey |
target, propertyKey, parameterIndex |
返回值 |
用返回值提供的构造函数来替换类的声明 |
返回值被用作方法的属性描述符 |
返回值被用作方法的属性描述符 |
被忽略 |
被忽略 |
参数释义:
constructor
: 类构造函数
target
: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
propertyKey
: 成员的名字
descriptor
: 成员的属性描述符
parameterIndex
: 参数在函数参数列表中的索引
Typescript 中的 Decorator 签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| interface TypedPropertyDescriptor<T> { enumerable?: boolean; configurable?: boolean; writable?: boolean; value?: T; get?: () => T; set?: (value: T) => void; }
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
|
Class 装饰器
类装饰器的类型定义如下:
1
| type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
|
class
装饰器应用于类的构造器,可用于观测、修改、替换类定义。
类装饰器只有一个参数: constructor
,constructor
为类的构造函数。类装饰器的返回值可以为空,也可以是一个新的构造函数。
扩展构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @name class Person { sayHi() { console.log(`My name is: ${this.name}`) } }
function name(constructor) { return class extends constructor { name = 'Niko' } }
new Person().sayHi()
|
修改原有属性的描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @seal class Person { sayHi() {} }
function seal(constructor) { let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHi') Object.defineProperty(constructor.prototype, 'sayHi', { ...descriptor, writable: false }) }
Person.prototype.sayHi = 1 // 无效
|
使用闭包来增强装饰器的功能
因为@
符号后边跟的是一个函数的引用,所以对于mixin的实现,我们可以很轻易的使用闭包来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class A { say() { return 1 } } class B { hi() { return 2 } }
@mixin(A, B) class C { }
function mixin(...args) { return function(constructor) { for (let arg of args) { for (let key of Object.getOwnPropertyNames(arg.prototype)) { if (key === 'constructor') continue Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key)) } } } }
let c = new C() console.log(c.say(), c.hi()) // 1, 2
|
覆盖旧的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function classDecorator<T extends { new (...args: any[]): {} }>( constructor: T) { return class extends constructor { newProperty = "new property"; hello = "override"; }; }
@classDecorator class Greeter{ property = "property";
hello: string;
constructor(m: string) { this.hello = m; } } const greeter: Greeter = new Greeter("world");
console.log({ greeter }, greeter.hello);
|
多个装饰器的应用
可以多个装饰器一起使用:
1 2 3
| @decorator1 @decorator2 class { }
|
执行的顺序为decorator2
-> decorator1
,离class
定义最近的先执行。
Method 装饰器
方法装饰器的类型定义如下:
1
| type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
|
方法装饰器有 3 个参数 target 、 propertyKey 和 descriptor。
- target: 静态方法是类的构造函数,实例方法是类的原型对象
- propertyKey: 方法名
- descriptor: 属性描述符 方法装饰器的返回值可以为空,也可以是一个新的属性描述符。
函数装饰是如果有返回值会默认作为属性的value
描述符存在,如果返回值为undefined
则会忽略,使用之前的descriptor
引用作为函数的描述符。
方案一 修改现有描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 表示方法的执行函数,用一个新的函数替换了原来值,新的方法还会调用原方法,只是在调用原方法前输出了一个日志。
方案二 返回新的value描述符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const Log: MethodDecorator = (target: any, key: string, descriptor: PropertyDescriptor) => { const className = target.constructor.name; const oldValue = descriptor.value; return { ...descriptor, value(...args) { console.log(`调用${className}.${key}()方法`); return oldValue.apply(this, args); } } };
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();
|
Property 装饰器
属性装饰器的类型定义如下:
1
| type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
|
属性装饰器有两个参数 target 和 propertyKey。
- target:静态属性是类的构造函数,实例属性是类的原型对象
- propertyKey:属性名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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
可以获得设置的必填的属性,从而对实例对象进行校验,校验相关的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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);
|
Parameter 装饰器
参数装饰器的类型定义如下:
1
| type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
|
参数装饰器有 3 个参数 target 、 propertyKey 和 descriptor。
- target:静态方法的参数是类的构造函数,实例方法的参数是类的原型对象
- propertyKey:参数所在方法的方法名
- parameterIndex:在方法参数列表中的索引值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| const parseConf = {} class Modal { @parseFunc addOne(@parse('number') num) { return num + 1 } }
function parseFunc (target, name, descriptor) { return { ...descriptor, value (...arg) { for (let [index, type] of parseConf) { switch (type) { case 'number': arg[index] = Number(arg[index]) break case 'string': arg[index] = String(arg[index]) break case 'boolean': arg[index] = String(arg[index]) === 'true' break } }
return descriptor.value.apply(this, arg) } } }
function parse(type) { return function (target, name, index) { parseConf[index] = type } }
console.log(new Modal().addOne('10'))
|
结合着函数装饰器来完成对函数参数的类型转换
访问符装饰器
访问符装饰器类型定义和 方法装饰器类型定义一样:
1
| type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
|
访问符装饰器的使用与方法装饰器一致,参数和返回值相同,只是访问符装饰器用在访问符声明之前。 需要注意的是,TypeScript 不允许同时装饰一个成员的 get 和 set 访问符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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]); }
|
MyClass 类中有一个属性 createDate 为 Date 类型, 另外增加一个有 get 声明的 createTime 方法,就可以以 entity.createTime 方式获得 createDate 的毫秒值。 但是 createTime 默认是不可枚举的,通过在声明前增加 @Enumerable 装饰器可以使 createTime 成为可枚举的属性。
给属性设置前缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Modal { _name = 'Niko'
@prefix get name() { return this._name } }
function prefix(target, name, descriptor) { return { ...descriptor, get () { return `wrap_${this._name}` } } }
console.log(new Modal().name)
|
静态类型和实例类型
默认Typescript属性是无法获取到的 descriptor
,但 静态属性是可以手动拿到的,但实例属性不行。
什么是静态属性和实例属性
1 2 3 4 5 6 7 8 9
| class Model { method1 () {} method2 = () => {}
static method3 () {} static method4 = () => {} }
|
method1
和method2
是实例成员,method1
存在于prototype
之上,而method2
只在实例化对象以后才有。
作为静态成员的method3
和method4
,两者的区别在于是否可枚举描述符的设置,所以可以简单地认为,上述代码转换为ES5版本后是这样子的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function Model () { this.method2 = function () {} }
Object.defineProperty(Model.prototype, 'method1', { value: function () {}, writable: true, enumerable: false, configurable: true })
Model.method4 = function () {}
Object.defineProperty(Model, 'method3', { value: function () {}, writable: true, enumerable: false, configurable: true })
|
可以看出,只有method2
是在实例化时才赋值的,一个不存在的属性是不会有descriptor
的,所以这就是为什么TS在针对Property Decorator
不传递第三个参数的原因,至于为什么静态成员也没有传递descriptor
.
就像上述的示例,我们针对四个成员都添加了装饰器以后,method1
和method2
第一个参数就是Model.prototype
,而method3
和method4
的第一个参数就是Model
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Model { @instance method1 () {} @instance method2 = () => {}
@static static method3 () {} @static static method4 = () => {} }
function instance(target) { console.log(target.constructor === Model) }
function static(target) { console.log(target === Model) }
|
静态属性手动获取 descriptor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Modal { @prefix static name1 = 'Niko' }
function prefix(target, name) { let descriptor = Object.getOwnPropertyDescriptor(target, name)
Object.defineProperty(target, name, { ...descriptor, value: `wrap_${descriptor.value}` }) return target }
console.log(Modal.name1)
|
装饰器执行顺序
不同声明上的装饰器将按以下顺序执行:
- 实例成员的装饰器:参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器
- 静态成员的装饰器:参数装饰器 > 方法装饰器 > 访问符装饰器/属性装饰器
- 构造函数的参数装饰器
- 类装饰器
如果同一个声明有多个装饰器,离声明越近的装饰器越早执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| function logClass1(params:string){ return function(target:any){ console.log('类装饰器1') } }
function logClass2(params:string){ return function(target:any){ console.log('类装饰器2') } }
function logAttribute1(params?:string){ return function(target:any,attrName:any){ console.log('属性装饰器1') } }
function logAttribute2(params?:string){ return function(target:any,attrName:any){ console.log('属性装饰器2') } }
function logMethod1(params?:string){ return function(target:any,attrName:any,desc:any){ console.log('方法装饰器1') } } function logMethod2(params?:string){ return function(target:any,attrName:any,desc:any){ console.log('方法装饰器2') } }
function logParams1(params?:string){ return function(target:any,attrName:any,desc:any){ console.log('方法参数装饰器1') } }
function logParams2(params?:string){ return function(target:any,attrName:any,desc:any){ console.log('方法参数装饰器2') } }
@logClass1('http://www.loaderman.com/api') @logClass2('xxxx') class HttpClient{ @logAttribute1() @logAttribute2() public apiUrl:string | undefined; constructor(){ }
@logMethod1() @logMethod2() getData(){ return true; }
setData(@logParams1() attr1:any,@logParams2() attr2:any,){
} }
var http:any=new HttpClient();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function f() { console.log("f(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("f(): called"); } } function g() { console.log("g(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("g(): called"); } } class C { @f() @g() method() {} }
|
结果:
1 2 3 4
| f(): evaluated g(): evaluated g(): called f(): called
|
装饰工厂🏭
由于每种装饰器都有它自身的调用签名,我们可以使用装饰器工厂来泛化装饰器调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import { logClass } from './class-decorator'; import { logMethod } from './method-decorator'; import { logProperty } from './property-decorator'; import { logParameter } from './parameter-decorator';
export function log(...args) { switch (args.length) { case 3: if typeof args[2] === "number") { return logParameter.apply(this, args); } return logMethod.apply(this, args); case 2: return logProperty.apply(this, args); case 1: return logClass.apply(this, args); default: throw new Error('Not a valid decorator'); } }
@log class Employee { @log private name: string;
constructor(name: string) { this.name = name; }
@log greet(@log message: string): string { return `${this.name} says: ${message}`; } }
|
装饰器的应用
使用装饰器可以实现自动注册路由,通过给 Controller 层的类和方法添加装饰器来定义路由信息,当创建路由时扫描指定目录下所有 Controller,获取装饰器定义的路由信息,从而实现自动添加路由。
装饰器代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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');
|
控制器代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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; } }
|
路由器代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| 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); }); } } }
|
创建路由代码
1 2 3 4 5 6 7 8 9
| import path from 'path'; import ScanRouter from './common/scanRouter';
const router = new ScanRouter();
router.scan([path.resolve(__dirname, './controller')]);
export default router;
|
Refer To
https://juejin.cn/post/6844903635168526343#heading-15
https://www.infoq.cn/article/ikvkdvq9haopv5yfppmj
https://www.typescriptlang.org/docs/handbook/decorators.html
https://yrq110.me/post/front-end/typescript-decorator-practice/