稀土掘金技术社区 2024年12月22日
都快2025年了,你们的前端代码都上装饰器了没?
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了TypeScript装饰器在前端开发中的实际应用,着重介绍了类装饰器、属性装饰器以及方法装饰器的使用场景和技巧。通过类装饰器,可以为类添加元数据配置,如标签和配置信息,避免硬编码魔法值。属性装饰器则可用于为类的属性添加配置,如表单和表格的配置,实现组件的自动配置和渲染。方法装饰器结合AOP编程范式,可以在不修改原有代码的情况下,实现权限校验等功能,提高代码的架构化和可维护性。文章还提及参数装饰器,并推荐了相关专栏文章和开源项目。

🏷️类装饰器:通过`@Label`和`@ClassConfig`装饰器,可以为类添加元数据配置,如标签和表格配置,避免在代码中出现魔法值,提高代码可维护性,并通过`Reflect`获取这些元数据。

🧰属性装饰器:通过`@Field`、`@Form`和`@Table`等装饰器,可以为类的属性添加配置,如表单验证规则、表格列的显示和行为,使得组件可以根据这些配置自动渲染和验证,减少重复代码。

⚙️方法装饰器:通过AOP编程范式和方法装饰器`@AdminRequired`,可以在方法执行前后添加额外的逻辑,如权限校验,避免修改原有方法代码,提高代码的模块化和可维护性,实现代码的切面化处理。

原创 Hamm 2024-12-22 09:02 北京

点击关注公众号,“技术干货”及时达!

点击关注公众号,“技术干货” 及时达!

可能很多人都听说过 TypeScript 的装饰器,也可能很多人已经在很多 NestJS 项目中使用装饰器了,也有一些前端开发者可能在某些前端框架中使用过一些简单的装饰器,那么你真的知道装饰器在前端还能玩出哪些花吗?

我们今天不讲基础概念,也不写一些你可能在很多文章里都看到过的没有意义的示例代码,我们直接拿装饰器来实战实现一些需求:

一、类装饰器

虽然很多前端对于类和面向对象是排斥的、抵触的,但不影响我们这篇文章继续来基于面向对象通过装饰器玩一些事情。

我已经写了很多关于面向对象在前端的使用了,实在是不想在这个问题上继续扯了,可以参考本专栏内的其他文章。

虽然但是,不论如何,你可以不用,但你不能不会,更不能不学。

不管在前端还是后端,我们可能都会用到类的实例来做一些事情,比如声明一个用户的类,让用户的类来完成一些事情。

我们可能会为类配置名称,比如给 User 类定义为 用户:

// 声明一个装饰器,用来保存类的文案
function Label(label: string{
  return (target: any) => {
    Reflect.set(target, "label", label)
  }
}
@Label("用户")
class User {
}

我们不限制被标记的类,你可以把any 用泛型约束一下,限制这个装饰器可以标记到哪些类的子类上。

我们可以通过 Reflect 来获取到类上的元数据,比如 Label 这个类上的 name 属性,通过 Reflect.getMetadata('name', User) 来获取到:

// 将打印 "用户"
console.log(Reflect.get(User, "label"))

通过这种方式,我们可以为类标记很多配置,然后在使用的时候就不会在代码里再出现很多类似 “用户” 的魔法值了。如果有改动的话,也只需要将 @Label("用户") 改成 @Label("XXX") 就好了。

当然,事实上我们不会单独为了一个小功能去声明一个装饰器,那样到时候会给类上标记很多的 @ 看着难受,于是我们可以直接声明一个 ClassConfig 的装饰器,用来保存类的各种配置:

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 这个类传过去,表格就自动知道没数据的时候应该显示什么文案了:

<Table :model="User" :list="list" />

上面的表格内部,可以获取 model 传入的类,再通过 Reflect 来获取到这些配置进行使用,如果没有配置装饰器或者装饰器没有传入这个参数,那么就使用默认值。

二、属性装饰器

很多人都知道,装饰器不仅仅可以配置到类上,属性上的装饰器用处更多。

这个和上面第一点中的一样,也可以为属性做一些配置,比如给用户的账号属性做配置,而且我们还可以根据主要功能来声明不同的装饰器,比如表单的 @Form,表格的 @Table 等等。

class User {
  @Field({
    label: "账号",
    // 如果下面的没有配置,那我来兜底。
    isEmail: true,
  })
  @Form({
    // 表单验证的时候必须是邮箱
    isEmail: true,
    // 表单验证的时候不能为空
    isRequired: true,
    placeholder: "请输入你牛逼的邮箱账号"
  })
  @Table({
    // 表示表格列的邮箱点击后会打开邮件 App
    isEmail: true,
    // 表格列的宽度
    width: 200,
    // 需要脱敏显示
    isMask: true
  })
  account!: string
}

当然,属性的装饰器声明和类的声明方式不太一致:

interface IFieldConfig {
  label?: string
  isEmail?: boolean
}
function Field(config: any{
  return (target: any, propertyKey: string) => {
    Reflect.set(target, propertyKey, config)
  }
}

使用 Reflect 获取的时候也不太一致:

const fieldConfig = Reflect.get(User.prototype, "account")
// 将打印出 `@Field` 配置的属性对象
console.log(fieldConfig)

想象一下,你封装的表格我也这么使用,我虽然没有传入有哪些表格列,但你是不是能够通过属性是否标记了 @Table 装饰器来判断是否需要显示这个邮箱列呢?

<Table :model="User" :list="list" />

你也可以再封装一些其他的组件,比如表单,比如搜索等等等等,像这样:

<Input :model="User" :field="account" />

上面的 Input 组件就会自动读取 User 这个类上的 account 属性的配置,然后根据配置来渲染表单和验证表单,是不是美滋滋?

三、方法装饰器和参数装饰器

这两个方式的装饰器我们在这篇文章不细讲,等装饰器这玩意在前端能被大家接受,或者前端娱乐圈骂面向对象不再那么狠的时候再细化一下吧,今天我们只讲讲简单使用:

3.1 方法装饰器

说到方法装饰器,我想先提一嘴 AOP 编程范式。

AOP(Aspect Oriented Programming) 是一种编程范式,它把应用程序切面化,即把应用程序的各个部分封装成可重用的模块,然后通过组合这些模块来构建应用程序。

举个简单的例子,我们最开始写好了很多代码和方法:

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")

以前调用这些方法都是正常的,突然有一天需求变了,只允许超级管理员才能调用这两个方法,你可能会这么写:

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 存在的意义,就是通过切面来修改已有的代码,比如在方法执行前,执行一段代码,在方法执行后,执行一段代码,在方法执行出错时,执行一段代码,等等。用更小的粒度来减少对已有代码的入侵。像这样:

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 的方式,在原有代码的基础上,添加了一些前置方法处理,所以看起来好像多了。但当我再加上一些后置的方法处理的话,代码量并没有添加多少,但结构会更清晰,代码入侵也没有。

「传统写法(入侵)」

class Test{
  张三的方法(){
    // 李四的前置代码
    // 张三巴拉巴拉写好的代码 
    // 李四的后置代码
  }
}

张三:“李四,你为什么用你的代码包围了我的代码!”

「装饰器写法(不入侵)」

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 中比较多,这篇文章就不过多介绍了,如果后续大伙有兴趣我们再聊。

四、总结

装饰器是一种新的语法,可以让你的前端代码更加的架构化,增加代码的可维护性。

如果你有兴趣,还可以阅读本专栏内的这些文章:

用TypeScript和装饰器优雅的为前端数据脱敏

TypeScript使用枚举封装和装饰器优雅的定义字典]

TypeScript中如何用装饰器替代JSON配置项封装表单

TypeScript装饰器之我们是这么做表单和校验的

当然,其他文章也很有意思哟~

今天就这样,欢迎继续关注我们的专栏 《用TypeScript写前端》

也欢迎关注我们的开源项目:AirPower4T,里面有很多装饰器在前端的应用场景,也许可以让你耳目一新。

Bye.

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

TypeScript 装饰器 AOP 前端开发 代码架构
相关文章