前言
亲爱的小伙伴,你好!我是 嘟老板。你是否用过 TypeScript
呢?对 装饰器 了解多少呢?有没有实践应用过呢?今天我们就来聊聊 装饰器 的那点事儿,看看它有哪些神奇的地方。
什么是装饰器
咱们先来看一段代码:
@Controller('/user')
export class UserController {
@Get('/queryList')
queryList() {
// 查询列表逻辑....
}
}
这段代码见于 Node
编写的服务端,其中 @Controller
注解定义了一个控制器,告诉框架 UserController
是一个可访问的服务端点。@Get
注解定义了一个 Get
方法访问的接口,可以通过 ${domainUrl}/user/queryList
访问 queryList
函数。
@Controller
和 @Get
就是本文的主人公 - 装饰器。
所以怎么定义 装饰器 呢?
装饰器 是 ECMAScript
即将推出的功能,允许我们以 可重用 的方式定义类和类成员。其本质上还是 函数,只不过是以一种特殊的方式使用而已。
这里有两个注意点:
- 装饰器 是应用到 类(class) 和 类成员 上的,比如 方法、属性、参数、访问器。相应的,装饰器 分为五类:类装饰器,属性装饰器,方法装饰器,参数装饰器,访问器装饰器,下文详细介绍。
- 以可重用的方式,什么意思?对于前端来说通常都会有
utils
,用于维护常用的工具集函数,避免重复造轮子。装饰器 对于类也起到类似的作用,将通用性逻辑抽离为装饰器函数,在需要的场景下使用。
装饰器 的使用比较简单, @
加上 装饰器函数 即可,如上面代码中的 @Controller()
。
装饰器怎么用
需要启用 experimentalDecorators 才能使用装饰器特性。
可以通过 命令行 或tsconfig
启用。
- 命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json
:{ compilerOptions: { target: "ES5", experimentalDecorators: true } }
创建装饰器函数
什么是装饰器函数?
@Controller
中的 Controller
就是装饰器函数,会在程序运行时执行。
那怎么创建装饰器函数呢?
- 直接创建
- 装饰器工厂创建
1.直接创建
装饰器函数说到底也只是和函数而已,只不过函数的参数是固定的,即目标代码的信息。
比如 @Controller
,咱先上一段代码,不要在意其合理性哈:
type TController = {new (...args: any[]): any}
function Controller<T extends TController>(BaseClass: T) {
return class extends BaseClass {
log() {
console.log('打印日志...')
}
}
}
@Controller
class UserController {}
(new UserController() as any).log()
执行以上代码,控制台打印如下:
OK,没啥问题。
2.装饰器工厂创建(Decorator Factory)
直接创建的装饰器,无法自定义处理逻辑,难以实现类似自定义参数的需求,如 @Get('/queryList')
。这就需要另一种创建方式 - 装饰器工厂。
装饰器工厂函数的返回值一个 装饰器函数。
上代码:
function Get(interfaceName: string): any {
return function(target: any) {
console.log(interfaceName)
}
}
@Controller
class UserController {
@Get('/queryList')
queryList() {}
}
new UserController()
控制台执行后,打印如下:
OK,没啥问题。
分类
1.类装饰器
简言之,在 类 上使用的装饰器就是类装饰器。
作用
可以监听、修改、替换声明的类。适用于继承现有类并添加适当的属性和方法。
类型声明
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
参数
target:现有类的构造函数。
返回值
若类装饰器返回一个值,则会替换原有类构造器的声明。
示例
type TBaseClass = {
new(...args: any[]): any
}
function ClassDecorator<T extends TBaseClass>(target: T): T {
return class extends target {
log() {
console.log('我是类装饰器')
}
}
}
class BaseClass {
log() {
console.log('我是 BaseClass')
}
}
@ClassDecorator
class ExampleClass extends BaseClass {}
new ExampleClass().log()
控制台执行结果:
OK,没啥问题。
2.属性装饰器
简言之,在 类属性 上使用的装饰器就是 属性装饰器。
作用
可以用来收集属性信息,为类添加额外的方法和属性。
类型声明
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
参数
- target:
若目标方法是 静态方法,则是类的构造器。
若目标方法是 实例方法,则是类的原型链。 - propertyKey: 目标属性的名称。
返回值
无返回值,若存在将被忽略。
示例
import 'reflect-metadata'
function transformPropertyToEvent(propertyKey: string) {
const firstLetterCapitalizedProp = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
return `on${firstLetterCapitalizedProp}Change`
}
function PropertyDecorator(target: any, propertyKey: string) {
const eventKey = transformPropertyToEvent(propertyKey)
target[eventKey] = function(fn: (pre: string, next: string) => void) {
let pre = this[propertyKey]
Reflect.defineProperty(this, propertyKey, {
set(val) {
fn(pre, val)
pre = val
}
})
}
}
class ExampleClass2 {
@PropertyDecorator
greeting = 'hello'
}
const ecInstance = new ExampleClass2()
// @ts-ignore
ecInstance.onGreetingChange((pre: string, next: string) => {
console.log(`pre: ${pre}; next: ${next}`)
})
ecInstance.greeting = 'hi'
控制台执行结果:
OK,没啥问题。
注:
上述示例代码中引入了reflect-metadata
库,这将添加一个 polyfill,用于支持使用 TS 实验性的元数据 API。
目前装饰器和装饰器元数据已经达到 stage3 阶段。
3.方法装饰器
简言之,在 类方法 上使用的装饰器就是 方法装饰器。
作用
可以修改或替换类方法原本的实现,添加一些通用逻辑等。
类型声明
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
参数
- target:
若目标方法是 静态方法,则是类的构造器。
若目标方法是 实例方法,则是类的原型链。 - propertyKey: 目标方法的名称。
- descriptor: 目标方法的描述器。
返回值
若方法装饰器返回一个值,则会替换该方法的描述器。
示例
相比 属性装饰器,方法装饰器 多了一个 descriptor
参数,可以通过该参数实现对于原方法的修改。
function MethodDecorator(target: any, propertyKey: string | symbol, descriptor: any) {
const originFn = descriptor.value
descriptor.value = function (...args: any[]) {
console.log('pre: MethodDecorator 开始打印日志')
originFn.apply(this, args)
console.log('post: MethodDecorator 打印日志结束')
}
}
class ExampleClass1 {
@MethodDecorator
log() {
console.log('我是 ExampleClass 的 log 方法')
}
}
new ExampleClass1().log()
控制台执行结果:
OK,没啥问题。
4.访问器装饰器
简言之,在 类访问器 上使用的装饰器就是 访问器装饰器。
什么是类访问器?
我们在定义类属性时,可能会用到类似以下的方式:
class Example {
innerValue = 123
get value() {
console.log(`get value: ${this.innerValue}`)
return this.innerValue
}
set value(val) {
console.log(`set value: ${val}`)
this.innerValue = val
}
}
这里的 get
和 set
就是 value
属性的访问器,可以分别为 value
属性的 获取 和 设置 添加自定义逻辑。
作用
访问器装饰器 和 方法装饰器 类似,可以修改或替换访问器原本的实现,添加一些通用逻辑等。
类型声明
declare type AccessorDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor<T>) => PropertyDescriptor<T> | void;
参数
- target:
若目标方法是 静态方法,则是类的构造器。
若目标方法是 实例方法,则是类的原型链。 - propertyKey: 目标方法的名称。
- descriptor: 目标方法的描述器。
访问器装饰器 的类型与 方法装饰器 的类型相似,不同之处在于 描述器(descriptor) 参数的 key
值
- 访问器装饰器
descriptor
key
:- get
- set
- enumerable
- configurable
- writable
- 方法装饰器
descriptor
key
:- value
- enumerable
- configurable
- writable
返回值
若方法装饰器返回一个值,则会替换该方法的描述器。
示例
function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: any) {
const originSetter = descriptor.set
descriptor.set = function(val: number) {
console.log(`set value: ${val}`)
return originSetter.call(this, val)
}
}
class ExampleClass3 {
private _value = 123
@AccessorDecorator
set value(val) {
this._value = val
}
get value() {
return this._value
}
}
const ec = new ExampleClass3()
ec.value = 234
控制台执行结果:
OK,没啥问题。
注意:
构造器装饰器 不能同时装饰单个成员的get
和set
访问器。而应该将所有装饰器都添加到该成员声明的第一个访问器上。
因为装饰器是应用于 属性描述符,而 描述符 中涵盖了get
和set
,不是单独声明。
5.参数装饰器
简言之,在 函数参数 前使用的装饰器就是 参数装饰器。经常用于类的 构造函数 或 类方法 中。
作用
通常用于收集参数信息,供其他装饰器使用。
类型声明
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) => void;
参数
- target:
若目标方法是 静态方法,则是类的构造器。
若目标方法是 实例方法,则是类的原型链。 - propertyKey: 属性名称(参数所在的 方法名,而不是参数名称)。
- parameterIndex: 参数在方法中的位置下标。
返回值
无返回值,若存在将被忽略。
示例
我们来实现一个 参数必填 的验证装饰器。
import "reflect-metadata";
const requiredMetadataKey = Symbol('required')
function Required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
const requiredParameters: number[] = Reflect.getMetadata(requiredMetadataKey, target, propertyKey) || []
requiredParameters.push(parameterIndex)
Reflect.defineMetadata(requiredMetadataKey, requiredParameters, target, propertyKey)
}
function Validate(target: Object, propertyKey: string | symbol, descriptor: any) {
const originFn = descriptor.value
descriptor.value = function (...args: any[]) {
const requiredParameters: number[] = Reflect.getMetadata(requiredMetadataKey, target, propertyKey)
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if ([undefined, null, ''].includes(args[parameterIndex])) {
throw new Error(`方法 ${String(propertyKey)} 缺少必填参数`)
}
}
}
return originFn.apply(this, args)
}
}
class ExampleClass4 {
@Validate
greet(@Required name: string) {
return `Hello, ${name}`
}
}
const ec = new ExampleClass4()
ec.greet('')
控制台执行结果: OK,没啥问题。
执行规则
1.应用时机
装饰器只会在 解释执行 应用一次。
例如:
function T(target: any) {
console.log('装饰器执行')
return target
}
@T
class EvaExampleClass1 {}
const eec = new EvaExampleClass1()
装饰器 T
中的 console
只会打印一次,不会因为 new
操作而再次打印。
2.执行顺序
不同类型的装饰器,有明确的执行顺序。
- 实例成员参数装饰器
- 实例成员方法/访问器/属性装饰器
- 静态成员参数装饰器
- 静态成员方法/访问器/属性装饰器
- 构造器参数装饰器
- 类装饰器
其中,
方法/访问器/属性装饰器 的执行顺序,按照其在类中的定义顺序而定。
同一方法中的不同参数的装饰器,按相反的顺序执行,最后一个参数的装饰器最先执行。
上代码验证一下:
function decorator(key: string): any {
console.log('装饰器应用: ', key);
return function () {
console.info('装饰器执行: ', key);
};
}
@decorator('类装饰器')
class EvaExampleClass2 {
@decorator('静态属性')
static prop?: number;
@decorator('静态方法')
static method(@decorator('静态方法参数:foo') foo: string, @decorator('静态方法参数:bar') bar: string) {}
constructor(@decorator('构造器参数') foo: string) {}
@decorator('实例方法')
method(@decorator('实例方法参数') foo: string) {}
@decorator('实例属性')
prop?: number;
}
执行结果:
3.组合装饰器
对同一目标同时使用多个装饰器,叫做 组合装饰器。比如同一个类方法添加多个 方法装饰器。
调用顺序如下:
- 应用外层装饰器
- 应用内层装饰器
- 调用内层装饰器
- 调用外层装饰器
例如:
function decorator(key: string): any {
console.log('应用: ', key);
return function () {
console.info('执行: ', key);
};
}
class EvaExampleClass3 {
@decorator('外层装饰器')
@decorator('内层装饰器')
method() {}
}
执行结果:
什么时候使用装饰器
结合以上介绍,简单列举一下 装饰器 的可能应用场景:
- 通用 Before/After 钩子
- 监听属性变更或方法调用
- 转换方法参数
- 给类添加额外的方法或属性
- 运行时类型检查
- 自动编码/解码
- 依赖注入
若小伙伴在实际应用中有更多合适的场景,可评论区留言讨论。
结语
好啦,今天的内容就到这里。本文从一个极简的 User
服务类切入,重点讲述 TS 装饰器 相关的知识点。如有疑问,欢迎评论区留言。
感谢阅读,愿 你我共同进步,谢谢!!!
往期推荐
- express 基础入门
- 一文带你了解多数企业系统都在用的 RBAC 权限管理策略
- 项目实战 | 如何恰当的处理 Vue 路由权限
- 项目实战 | 如何正确使用 watch/computed/ref