Vue3面向对象编程

5,550 阅读4分钟

先讲一下前提,本人在业务中主要使用 vue3 + tsx 进行开发

为什么不使用模板?

  1. 上下文不一致
  2. ref隐式value转换
  3. 模板中类型提示弱鸡,个人非常依赖IDE的智能提示
  4. 模板经常更新的语法,隔一段时间模板的写法就变了,心累

Tsx用户体验问题

  1. 无法显示的声明组件props,导致需要写很多声明属性的代码
  2. 无法定义泛型组件
  3. ref在render函数中需要时刻写上 .value
  4. slots没有地方声明类型
  5. 组件expose出去的数据及方法也没有类型

下面介绍在vue中使用类来解决上面提到的这些问题, 主要依赖的包在 vue3-oop,可以查看例子 stackblitz.com/edit/vite-y…

Vue类组件

import { Autobind, ComponentProps, Computed, Hook, Link, Mut, VueComponent } from 'vue3-oop'
import { Directive, VNodeChild, watch } from 'vue'

const focusDirective: Directive = {
  mounted(el: HTMLInputElement) {
    el.focus()
  },
}

// 直接显示定义组件属性
interface Foo_Props {
  size: 'small' | 'large'
  // 组件的slots定义类型
  slots: {
    item(name: string): VNodeChild
  }
}

class Foo extends VueComponent<Foo_Props> {
  // vue需要的运行时属性检查
  static defaultProps: ComponentProps<Foo_Props> = {
    size: String,
  }
  // 组件需要的局部指令
  static directives: Record<string, Directive> = {
    focus: focusDirective,
  }
  constructor() {
    super()
    // watch在构造函数中初始化
    watch(
      () => this.count,
      () => {
        console.log(this.count)
      },
    )
  }

  // 组件自身状态,可以理解为所有在UI中用到的变量都得加上这个装饰器,表明我需要UI刷新
  @Mut() count = 1
  // 计算属性
  @Computed()
  get doubleCount() {
    return this.count * 2
  }
  add() {
    this.count++
  }
  // 自动绑定this
  @Autobind()
  remove() {
    this.count--
  }

  // 生命周期
  @Hook('Mounted')
  mount() {
    console.log('mounted')
  }

  // 对元素或组件的引用
  @Link() element?: HTMLDivElement
  
  @Mut() list = [{a: 1}]
  
  @Link() child?: FooChild // 引用组件是有类型的

  render() {
    return (
      <div ref="element">
        <span>{this.props.size}</span>
        <button onClick={() => this.add()}>+</button>
        <span>{this.count}</span>
        <button onClick={this.remove}>-</button>
        <div>{this.context.slots.item?.('aaa')}</div>
        <input type="text" v-focus />
        <FooChild ref="child" data={list} v-slots={{item(k){ return <span>{k.a}</span>}}}></FooChild>  
      </div>
    )
  }
}

// 泛型组件
interface FooChild_Porps<T> {
  data: T[]
  onClick: (item: T) => T
  slots: {
    item(item: T): VNodeChild
  }
}
class FooChild<T> extends VueComponent<FooChild_Porps<T>> {
  add(){
  }
  render() {
      return <ul>
      {
    	this.props.data.map(k => <li onClick={() => this.props.onClick?.(k)}>{this.context.slots.item?.(k)}</li>)
      }
      </ul>
  }
}


其实主要的Api很少

API类型作用
VueComponent组件必须集成及实现render方法
VueService服务需要继承
Mut属性装饰器标记属性为响应式的
Computed方法装饰器标记属性为计算属性
Hook方法装饰器声明周期需要执行的函数
Link属性装饰器引用子组件或者dom元素
Autobind方法装饰器绑定this到函数

通过类组件我们完美的解决了上面提到的所有问题,IDE也有很完善的智能提示。

类服务 = vue hooks

想想vue hooks的本质是什么,他封装的是变量改变变量的函数

function useState() {
    const count = ref(0)
    function add() {
        count.value++
    }
    return { count, add }
}

那这个和类有什么本质的区别么,我觉得类主要是把 作用域 这个东西显式的用this暴露出来了, 而hooks用闭包实现了免this调用,但失去了类型,我们来看看如何定义一个服务以及使用吧

class CountService extends VueService {
  @Mut() count = 1
  add() {
    this.count++
  }
  remove() {
    this.count--
  }
}

class Foo extends VueComponent {
  countService = new CountService() // 手动new
}

状态管理

vue3有了原生的 provide/inject,在vue生态里面那些一堆的状态管理包就此没有用武之地了,我们来看看怎么使用

class CountService extends VueService {
  // 类上有这个key, 会在构造函数中直接 provide
  static ProviderKey: InjectionKey<CountService> = Symbol()
  @Mut() count = 1
  add() {
    this.count++
  }
  remove() {
    this.count--
  }
}

class Foo extends VueComponent {
  countService = new CountService() // 手动new
  render() {
      return 
  }
}

class FooChild extends VueComponent {
    countService = inject(CountService.ProviderKey)
    render() {
        return <div>{this.countService.count}</div>
    }
}

自动化的依赖注入

前面我们看到服务都是自己手动new的,以及注入的服务也是手动注入进来的,要提高开发用户体验,程序员怎么能玩手动呢?我们使用angular的 injection-js 来管理我们的服务,让他来作为IOC容器

import { VueComponent, VueService } from 'vue3-oop'
import { Injectable } from 'injection-js'

class CountService extends VueService { 
	@Mut() count = 1
}

@Injectable() // 有需要注入的服务时写上
class BarService extends VueService {
  constructor(private countService: CountService) {super()}
}

// 组件注入服务
@Component()
class Bar extends VueComponent {
  // 构造函数参数声明需要的服务
  constructor(
    private countService: CountService, 
    private barService: BarService
  ) { super() }

  render() {
    return <div>{this.countService.count}</div>
  }
}

这样我们就用 class + tsx + di 实现了类Angular的开发体验,同时比angular更简单,对此感兴趣的同学可以移步 vue3-oop 查看

结语

随着前端项目规模的增大,我们急需一种干净的架构来知道前端的开发,而领域驱动设计(DDD)是在后端发展已久的一种架构,DDD依赖OOP,有了OOP我们就可以进行领域的设计,而angular的高上手难度劝退了不少人,但angular的架构思想的光辉不能被遗忘,所以在vue上实现了类似angular的架构,让更多前端人接触到OOP,写出来更好的代码。