likes
comments
collection
share

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

作者站长头像
站长
· 阅读数 38

一开始以为动态创建的class(或者对象)只能使用 any,不能支持 Typescript 呢,后来深入了解了一下,发现 class 也是支持泛型的。那么我们是不是可以发挥一下泛型的作用?

让动态类型也可以支持 Typescript

核心思路

  • 使用 class + 泛型 T
  • 做个语法糖,合并类型

强类型语言的风格,定义时指定属性

我们先按照强类型语言的风格定义一个class:

/**
 * 强类型风格的 class
 */
export default class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  set $state(val: Person) {
    Object.assign(this, val)
  }
}

定义了两个属性,name 和 age,然后在初始化(构造函数)里面使用参数给属性赋值,然后再设置几个方法。这样一个简单的 class 就完成了。

我们来看一下使用方式:

  import Person from './_class-person'
  const person = new Person( '我是强类型风格的class', 10 )
  console.log('person', person)
  
  // 如果类型不匹配,会出现警告
  person.$state = {
    name: '改个名字'
  }

使用的时候也比较简单,先 new 一下,传入参数即可。class 也是一种类型

  • 我们看一下结构:

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

  • 类型不匹配的警告:

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

js风格,动态创建属性

上面的代码看起来比较繁琐,写起来也比较累人,因为每一个对象都需要定义一个class,并不符合 js 的风格。 所以我们创建一个符合 js 风格的 class:使用了泛型

/**
 * 给对象加上辅助功能:$state 
 * @param val 初始值,对象
 */
export default class BaseObject<T extends object> {
  
  constructor( val: T ) {
    // 设置具体的属性,浅层拷贝
    Object.assign(this, val)
  }

  /**
   * 设置新值
   */
  set $state(value: T) {
    // 要不要判断 value 的属性是否完整?
    Object.assign(this, value)
  }
}

定义 class 的时候并没有指定属性,而是通过传入的参数动态创建属性。 需要传入一个对象(使用泛型约束),把对象的属性通过 Object.assign(this, val) 设置给 class。

class 也是可以支持泛型的,我们利用泛型来实现动态的类型推断。 $state 的参数 value 的类型,可以使用泛型 T 来约束,这样赋值的时候也可以进行类型检查。

我们来看一下使用方式

  import Base from './_class-base'
  
  const base = new Base({
    name: '我是js风格的class',
    age: 18
  })
  console.log('base', base)
  // 可以做类型检查
  base.$state = {
    name: '改个名字'
  }
  base.name = 'ts 不识别泛型的属性'

可以直接传入一个对象,然后使用 class 提供的方法,可以检查参数类型,但是无法识别泛型的属性。

  • 看一下结构

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

这个结构和直接指定属性的class的结构是一样的。

  • 类型不匹配的警告

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

  • 没有识别泛型的属性

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

既然识别泛型了,为啥没有把T结合起来?或者是我没发现正确的打开方式?

合并类型

不过这样还是不够,虽然限定了 $state 的值,但是没有识别泛型的属性,这还差点意思。 只是目前没找到更好的方法,所以只好用一种粗暴的方式:强行合并类型!

/**
 * 语法糖,合并 class 和 T
 * @param val 对象
 * @returns 
 */
export default function Model<T extends object> (val: T) {
  const re = new Base<T>(val)
  return re as T & Base<T>
}

把泛型 T 和 class 的类型合并,这样就可以返回一个合并后的类型。

使用方式

  import Model from './_class-model'

  const model = Model({
    name: '我是语法糖,合并类型',
    age: 18
  })
  console.log('model', model)

  // 可以做类型检查
  model.$state={
    name: '改个名字'
  }
  model.name = 'ts 可以识别了'

经过语法糖的白装,既可以进行类型判断,也可以识别泛型的属性了。

这种用法看起来是不是和 Vue3 的 reactive 有点像?

套上 reactive 实现响应性

说起了Vue3的 reactive,就不得不提到响应性,那么这个是否也可以支持一下响应性呢? 当然是可以的,只需要把实例套上 reactive 即可,同理我们还需要做一个语法糖。

/**
 * 语法糖,合并 class 和 T,套上 reactive 实现响应性
 * @param val 对象
 * @returns 
 */
export default function Model<T extends object> (val: T) {
  const re = new Base<T>(val)
  const ret = reactive(re)
  return ret as T & Base<T>
}

还是一样的操作方式,把泛型 T 和 class 的类型合并,这样就可以返回一个合并后的类型。

使用方法

  import Model from './_class-model2'
  
  const model2 = Model({
    name: '我是套壳的 reactive',
    age: 18
  })
 
  // 可以检查类型
  model2.$state = {
    name: '直接赋值也用响应性'
  }
  model2.name = '有响应性了'

写到这里大家应该可以发现,我为啥要设置 $state 这个方法了吧,你猜对了,就是想实现 reactive 的直接赋值的目的。

  • 看看结构

【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject

经过一系列的折腾,使用方式和 reactive 很像了,内部使用了 reactive,所以也具有了 reactive 的功能。 另外,你又猜对了,$state 是参考了 Pinia 的一个同名方法。

依赖注入如何获取类型?

提到 Vue3 就不得不说说 provide/inject。放进去前类型好好的,取出来之后类型呢?

为了避免这样的问题发生,Vue3 提供了 InjectionKey

我们封装一下 provide/inject:

  • ./50-config
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
import type BaseClass from './_class-base'

export type IPerson = {
  name: string,
  age: number
}

const key = Symbol() as InjectionKey<BaseClass<IPerson> & IPerson>

// 父组件提供
export function regPerson(value: BaseClass<IPerson> & IPerson) {
  provide(key, value)
}

// 子组件获取
export function getPerson() {
  const model = inject(key) as BaseClass<IPerson> & IPerson
  return model
}
  • provide, inject Vue 提供的依赖注入,可以方便父子组件之间共享数据。官网
  • InjectionKey Vue 提供的一个 InjectionKey 接口,它是一个继承自 Symbol 的泛型类型,可以用来在提供者和消费者之间同步注入值的类型。(来自官网
  • IPerson 根据需要定义的类型。
  • key 用 Symbol() 设置一个带类型的标识,不用担心重名问题。
  • regPerson 父组件通过 provide 提供共享数据。
  • getPerson 子组件通过 inject 得到父组件的共享数据。

这样,子组件得到的父组件的共享数据,也带上了类型。

依赖注入也可以支持泛型

上面的代码虽然实现了需求,但是需要针对每一个具体的类型做一套封装函数,这就又繁琐了!其实依赖注入也是支持泛型的。

  • ./50-config
const key2 = Symbol()

// 父组件提供
export function regPerson<T extends object>(value: BaseClass<T> & T) {
  const key = key2 as InjectionKey<BaseClass<T> & T>
  provide(key, value)
}

// 子组件获取
export function getPerson<T extends object>() {
  const key = key2 as InjectionKey<BaseClass<T> & T>
  const model = inject(key) as BaseClass<T> & T
  return model
}

首先需要定义一个 Symbol 作为识别标识,然后用 InjectionKey 携带类型。这个识别标识要放在函数的外面,如果放在函数里面,那么每次调用函数,都会得到一个新的 Symbol,这样 getPerson 函数就无法获取父组件共享的数据了。

getPerson 函数设置了一个泛型 T,然后泛型 T 传给 InjectionKey,这样就避免了每个类型都要写一遍的尴尬。

这样不管是什么类型,都可以使用这套封装的函数来实现依赖注入的功能,而且也不用担心重名的问题。

使用方式

  • 父组件
  import { regPerson } from './50-config'
  import type { IPerson } from './50-config'
  // 定义model,见上
  // 提供给子组件
  regPerson<IPerson>(model)

这样代码就很清爽了,不用关心实现的细节问题,只需要关心需要使用哪种类型即可。

  • 子组件
  import { getPerson } from './50-config'
  import type { IPerson } from './50-config'
  
  const model = getPerson<IPerson>()

  // 可以做类型检查
  model.$state = {
    name: 'ok',
    age:2
  }
  model.name = '子组件获取后,也可以带上类型'

知道类型就可以获取父组件共享的数据,其他细节都可以不用去关心,开袋即食。

小结

总结一下本章实现了哪些功能:

  • 可以复用代码,比如 $state ,实现整体赋值等功能。
  • 使用 class + 泛型 + 类型合并,动态创建的 class 也可以很好的支持 Typescript 的类型检查。
  • 通过套壳 reactive 实现了响应性。
  • 通过泛型的 provide/inject,支持依赖注入的类型携带。

不断和自己较劲,不断完善细节,不断的优化代码,提高、进步每一天。

源码

gitee.com/naturefw-co…

在线演示

naturefw-code.gitee.io/nf-rollup-s…

转载自:https://juejin.cn/post/7238787008260800572
评论
请登录