前端开发使用装饰器和Reflect,开始不一样的开发体验

536 阅读4分钟

一、前言

在之前的 《用TypeScript写前端》 专栏文章中,我们已经讲过了大量装饰器来配置表格、表单、文案、验证等相关的文章。

今天我们主要来讲一下如何通过 Relect 来存储装饰器传入的各种配置项,以及在何时取出配置项来使用。

二、先看示例

例如我们声明了一个 角色类RoleEntity 对象:

@Model({
  label: '角色',
})
export class RoleEntity extends BaseEntity {
  @Table({
    forceShow: true,
  })
  @Search()
  @Form({
    requiredString: true,
  })
  @Field({
    label: '角色名称',
  })
    name!: string

  @Table({
    forceShow: true,
  })
  @Search()
  @Form({
    placeholder: '不填按编码规则自动生成',
  })
  @Field({
    label: '角色编码',
  })
    code!: string

  @Field({
    type: MenuEntity,
    array: true,
  })
    menuList!: MenuEntity[]
    
  @Field({
    type: PermissionEntity,
    array: true,
  }) permissionList!: PermissionEntity[]
}

接下来,我们假设从某个地方得到了一个 RoleEntity 对象,我们打印出来看看:

const role = new RoleEntity()
console.log(role)

QQ_1739618411682.png

咦,为什么打印出来的原型上还挂了这么多的东西?这都是些什么玩意?

原来,上面的 RoleEntity 类中装饰器配置的所有属性,都被通过 Reflect 完整的存在了对象的原型上。

Reflect 是一个内置对象,它提供了一些方法,用于操作对象。这些方法与 Object 的方法类似,但更安全,更易用。

这里我们需要使用到 Reflect 的两个方法:

三、Reflect.defineProperty

如其名,表示我们将定义一个属性值,于是我们可以在装饰器中调用这个方法,来把装饰器的参数都存到对象的原型上。

/**
 * 反射添加属性
 * @param target 目标类
 * @param key 属性名称
 * @param value 装饰器的参数
 */
Reflect.defineProperty(target, key, {
  enumerable: false,
  value,
  writable: false,
  configurable: true,
})

这样,我们就成功的存储的装饰器的配置,那么具体要存些什么你就可以自由发挥啦。

我们存储了下面这些配置:

3.1 类的信息

我们存储了这些信息:

export interface IModelConfig {
  /**
   * ### 模型名称
   */
  label?: string

  /**
   * ### 模型属性前缀
   */
  fieldPrefix?: string

  /**
   * ### 是否显示搜索框
   */
  showSearch?: boolean

  /**
   * ### 添加按钮的标题
   */
  addTitle?: string

  /**
   * ### 添加权限标识 还有各种增删改查等通用的权限表示的配置
   */
  addPermission?: string

  /**
   * ### 全局隐藏字段列选择器
   */
  hideFieldSelector?: boolean
}

3.2 属性的信息

例如 UserEntitynickname 属性,我们存储了关于属性的各种表单、表格、搜索等配置:

3.2.1 表单信息

包含了属性是否必填、输入格式、是否下拉框、可选词典、默认值、输入提示信息等等

3.2.2 表格信息

包含了属性是否显示到表格列、列宽、列排序、数据格式、是否字典、输入类型(如数字、字符串、日期等)、对齐方式、是否可复制等信息

同时还有存储一个数组用于表示,哪些列配置了装饰器。于是你的表格组件可以取出来自动渲染列了。

3.2.3 搜索信息

包含了是否展示搜索框到表格顶部,也支持了很多关于搜索的数据类型等信息。

3.3 存储配置的意义

存储这些配置,是为了在 TableFormSearch 等组件中,可以取出这些配置,然后进行各种渲染。

接下来,我们来看看如何取出这些配置并使用:

3.4 Reflect.get

Reflect.get 方法用于读取对象的属性值。

我们可以封装一个组件,比如 Table,要求它接受一个 Entity 类的参数,于是使用时即可这样:

<Table :entity="UserEntity" />

接下来,表格组件内部即可通过 UserEntity 获取到所有的表格列、列的具体配置,来完成表格的正常渲染:

// 举个例子
const fieldConfigs = Reflect.get(UserEntity, 'fields')

然后就可以通过循环渲染表格列了。

四、为何要这么使用

回到最开始的截图中,我们可以看到,当我们拿到一个RoleEntity对象时,它上面挂了很多东西,这些东西都是通过装饰器来配置的。

于是我们可以在调试阶段清楚的看到这些配置的信息,也可以在组件中取出这些配置,然后完成更多的事情。

数据对象不在是纯粹的数据信息,也同时包含了这个数据对象的一些其他的配置信息,我们在参数传递的时候就能同时也传递这些配置,可以实现更多更方便的功能。

而不是当我需要你渲染一个表格的时候,我会用下面传统的方式提供给你多个参数:

interface User {
  name: string
  age: number
}
const list: Ref<User[]> = ref([
  {
    name: '张三'
    age: 18
  }
])
const fields = ref([
  {
    field: 'name',
    label: '姓名'
  },
  {
    field: 'age',
    label: '年龄'
    isNumber: true
  }
])

我们也只是将 User 结构的声明和 fields 的配置通过 类和装饰器 放到了一起而已:

@Model({label: '用户'})
class User {
  @Field({label: '姓名'})
  name!: string

  @Field({label: '年龄', isNumber: true})
  age!: number
}

五、总结

今天算是 2025 年的开篇,祝大家新的一年开心快乐~

昨天的文章是个意外~~~