这篇文章主要介绍了 TypeScript 装饰器在前端的应用,包括类装饰器、属性装饰器、方法装饰器和参数装饰器。类装饰器可标记类的配置,属性装饰器能为属性做配置。方法装饰器以 AOP 编程范式为例,减少对原有代码的入侵。
可能很多人都听过 TypeScript 的装饰器,也可能很多人已经在很多 NestJS 项目中使用装饰器了,也有一些前端开发者可能在某些前端框架中使用过一些简单的装饰器。
今天直接拿装饰器来实战实现一些需求:
一、类装饰器
不管在前端还是后端,我们可能都会用到类的实例来做一些事情,比如声明一个用户的类,让用户的类来完成一些事情。
我们可能会为类配置名称,比如给 User
类定义为 用户
:
javascript
// 声明一个装饰器,用来保存类的文案
function Label(label: string) {
return (target: any) => {
Reflect.set(target, "label", label)
}
}
@Label("用户")
class User {
}
我们不限制被标记的类,你可以把any 用泛型约束一下,限制这个装饰器可以标记到哪些类的子类上。
我们可以通过 Reflect
来获取到类上的元数据,比如 Label
这个类上的 name
属性,通过 Reflect.getMetadata('name', User)
来获取到:
javascript
// 将打印 "用户"
console.log(Reflect.get(User, "label"))
通过这种方式,我们可以为类标记很多配置,然后在使用的时候就不会在代码里再出现很多类似 "用户" 的魔法值了。如果有改动的话,也只需要将 @Label("用户")
改成 @Label("XXX")
就好了。
当然,事实上我们不会单独为了一个小功能去声明一个装饰器,那样到时候会给类上标记很多的 @
看着难受,于是我们可以直接声明一个 ClassConfig
的装饰器,用来保存类的各种配置:
javascript
interface IClassConfig {
// 刚才的 label
label?: string
// 添加一些其他的配置
// 表格的空数据文案
tableEmptyText?: string
// 表格删除提醒文案
tableDeleteTips?: string
}
function ClassConfig(config: IClassConfig){
return (target: any) => {
Reflect.set(target, "config", config)
}
}
@ClassConfig({
label: "用户",
tableEmptyText: "用户们都跑光啦",
tableDeleteTips: "你确定要删除这个牛逼的人物吗?"
})
当然,我们可以通过 Reflect.getMetadata('config', User)
来获取到 ClassConfig
这个类上的配置,然后就可以在代码里使用这些配置了.
比如,我们还封装了一个 Table 组件,我们就只需要将
User
这个类传过去,表格就自动知道没数据的时候应该显示什么文案了:
javascript
<Table :model="User" :list="list" />
上面的表格内部,可以获取 model
传入的类,再通过 Reflect
来获取到这些配置进行使用,如果没有配置装饰器或者装饰器没有传入这个参数,那么就使用默认值。
二、属性装饰器
很多人都知道,装饰器不仅仅可以配置到类上,属性上的装饰器用处更多。
这个和上面第一点中的一样,也可以为属性做一些配置,比如给用户的账号属性做配置,而且我们还可以根据主要功能来声明不同的装饰器,比如表单的 @Form
,表格的 @Table
等等。
javascript
class User {
@Field({
label: "账号",
// 如果下面的没有配置,那我来兜底。
isEmail: true,
})
@Form({
// 表单验证的时候必须是邮箱
isEmail: true,
// 表单验证的时候不能为空
isRequired: true,
placeholder: "请输入你牛逼的邮箱账号"
})
@Table({
// 表示表格列的邮箱点击后会打开邮件 App
isEmail: true,
// 表格列的宽度
width: 200,
// 需要脱敏显示
isMask: true
})
account!: string
}
当然,属性的装饰器声明和类的声明方式不太一致:
javascript
interface IFieldConfig {
label?: string
isEmail?: boolean
}
function Field(config: any) {
return (target: any, propertyKey: string) => {
Reflect.set(target, propertyKey, config)
}
}
使用 Reflect
获取的时候也不太一致:
javascript
const fieldConfig = Reflect.get(User.prototype, "account")
// 将打印出 `@Field` 配置的属性对象
console.log(fieldConfig)
想象一下,你封装的表格我也这么使用,我虽然没有传入有哪些表格列,但你是不是能够通过属性是否标记了 @Table
装饰器来判断是否需要显示这个邮箱列呢?
javascript
<Table :model="User" :list="list" />
你也可以再封装一些其他的组件,比如表单,比如搜索等等等等,像这样:
javascript
<Input :model="User" :field="account" />
上面的 Input 组件就会自动读取 User
这个类上的 account
属性的配置,然后根据配置来渲染表单和验证表单,是不是美滋滋?
三、方法装饰器和参数装饰器
这两个方式的装饰器今天我们只讲讲简单使用:
3.1 方法装饰器
说到方法装饰器,我想先提一嘴 AOP
编程范式。
AOP(Aspect Oriented Programming) 是一种编程范式,它把应用程序切面化,即把应用程序的各个部分封装成可重用的模块,然后通过组合这些模块来构建应用程序。
举个简单的例子,我们最开始写好了很多代码和方法:
javascript
class User {
add(name: string) {
console.log("user " + name + " added!")
}
delete(name: string) {
console.log("user " + id + " deleted!")
}
}
const user = new User();
user.add("Hamm")
user.delete("Hamm")
以前调用这些方法都是正常的,突然有一天需求变了,只允许超级管理员才能调用这两个方法,你可能会这么写:
javascript
class User {
add(name: string) {
checkAdminPermission()
console.log("user " + name + " added!")
}
// 其他方法
}
function checkAdminPermission() {
if(!你的条件){
throw new Error("没有权限")
}
}
const user = new User();
user.add("Hamm")
虽然也没毛病,但需要去方法内部添加代码,这属于改动了已有的逻辑。
而 AOP 存在的意义,就是通过切面来修改已有的代码,比如在方法执行前,执行一段代码,在方法执行后,执行一段代码,在方法执行出错时,执行一段代码,等等。用更小的粒度来减少对已有代码的入侵。像这样:
javascript
class User {
@AdminRequired
add(name: string) {
console.log("user " + name + " added!")
}
}
function AdminRequired(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
if (你的条件) {
return originalMethod.apply(this, args)
}
throw new Error("没有权限")
}
}
const user = new User()
console.log(user.add("Hamm"))
乍一看,我就知道又会有人说:"你这代码不是更多了么?" 看起来好像是。
但事实上,从代码架构上来说,这没有对原有的代码做任何改动,只是通过 AOP 的方式,在原有代码的基础上,添加了一些前置方法处理,所以看起来好像多了。但当我再加上一些后置的方法处理的话,代码量并没有添加多少,但结构会更清晰,代码入侵也没有。
传统写法(入侵)
javascript
class Test{
张三的方法(){
// 李四的前置代码
// 张三巴拉巴拉写好的代码
// 李四的后置代码
}
}
张三:"李四,你为什么用你的代码包围了我的代码!"
装饰器写法(不入侵)
javascript
class Test {
@LiSiWantToDoSomething
张三的方法() {
// 张三巴拉巴拉写好的代码
}
}
function LiSiWantToDoSomething(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value
descriptor.value = function (...args: any[]) {
console.log("李四的前置代码")
const result = originalMethod.apply(this, args)
console.log("张三干完了,结果是" + result)
return "我是李四,张三的结果被我偷走了"
}
}
这时,张三的代码完全在不改动的情况下添加了前置和后置代码。
3.2 参数装饰器
参数装饰器的使用场景在前端比较少,在 Nest 中比较多,就不过多介绍了。
四、总结
装饰器是一种新的语法,可以让你的前端代码更加的架构化,增加代码的可维护性。