【vue3】运行时创建的 class 也可以支持Typescript,同时支持 reactive 和 provide/inject
一开始以为动态创建的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 也是一种类型
- 我们看一下结构:
- 类型不匹配的警告:
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 提供的方法,可以检查参数类型,但是无法识别泛型的属性。
- 看一下结构
这个结构和直接指定属性的class的结构是一样的。
- 类型不匹配的警告
- 没有识别泛型的属性
既然识别泛型了,为啥没有把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 的直接赋值的目的。
- 看看结构
经过一系列的折腾,使用方式和 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,支持依赖注入的类型携带。
不断和自己较劲,不断完善细节,不断的优化代码,提高、进步每一天。
源码
在线演示
转载自:https://juejin.cn/post/7238787008260800572