有必要将Vue项目重构到Typescript吗?

Vue对Typescript的支持

与React不同的是,Vue的组件在运行时是动态生成的,换句话说就是在非runtime环境是无法知道具体变量的类型的,而React尤其是使用了ES6class作为组件的React在做静态类型检查的时候会有天生的优势,也就是这个原因导致tsx的支持非常的迅速。
虽然Vue在2.5.0版本中为Vuevue-routevuex等库加入了大量的type declarations,但是Typescript依然无法轻易的推断出Vue中用到的基于对象的Api中的this,换个通俗点的说法就是以前用Typescript难以推断用Object的写法写出的Vue组件中的this

vue-class-component

为了解决这个问题,Vue官方推出了一个名为vue-class-component的库,该库提供了一个名为Component的装饰器,目的在于能让开发者用class的语法去写Vue组件。
大致会发生如下改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 改造前
<script>
import someComponent from 'path/to/component'
export default {
props: { propsA: { type: String, required: true } },
component: { someComponent },
data() {
return {
dataA: '',
}
},
computed {
computedA() { return this.dataA + 'wahaha' }
}
methods: {
foo () {
alert(this.dataA)
}
}
mounted () {
this.foo()
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 改造后
<script lang='ts'>
import SomeComponent from 'path/to/component'
import Vue from 'vue'
import Component from 'vue-class-component'
@Component({
props: { propsA: { type: String, required: true } },
component: { someComponent }
})
export default class Foo extends Vue {
propMessage!: string
dataA: string = ''
get computedA(): string { return this.dataA + 'wahaha' }
foo () {
alert(this.dataA)
}
mounted () {
this.foo()
}
$refs!: {
SomeComponent: SomeComponent
}
}
</script>

看起来是不是很熟悉很简单,但是实际上不是这样的,从官方的例子我们能看出该库有了如下改变规则

  • 将class的成员方法直接作为组件的methods
  • 与生命周期钩子同名的成员方法将作为生命周期函数
  • 组件内部成员变量作为响应式数据,同时可以声明类型
  • 计算属性改为get属性
  • 使用了$refs占位符的动态组件,需要声明类型
  • props也需要声明类型

如果仔细一点,你会发现,我的props在两个地方被声明了类型。不难看出,作为装饰器的参数的props其实是vue组件本身自带的类型检查,而在class内部声明的类型,是交给typescript做类型检查的。
这意味着,我们在重构代码时需要做一些抉择:究竟是否放弃Vue本身的类型检查。

同时,vue组件的选项远不止上述代码展示的这么点,vue-class-component的官方文档中这样写道:

For all other options, pass them to the decorator function.

刚看到这里的时候我以为官方已经为开发者做好了一切,然而在继续翻阅文档和源码的过程中我发现并非这样,官方提供了一种自己createDecorator的方法,也就是自己动手的意思。
好在有一个名为vue-property-decorator的库已经在vue-class-component的基础上封装了常用的vue组件选项,这个将在下文中讨论。

当然也有人会好奇,用新的类的写法,能否像react一样将类的方法写成箭头函数的形式呢。很遗憾依然不行,箭头函数中的this依然没有指向vue实例,这意味着在箭头函数内部使用this.dataName这种方式来修改响应式数据是不会生效的。

vue-property-decorator

vue-property-decorator 为开发者提供了7中装饰器,他们分别是

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component

体验了一番下来,让我最感到奇怪的是@Emit这个装饰器,例如我原本要emit一个click事件

1
2
3
4
5
6
7
8
// 按照原本vue的写法
onFooClick(foo) {
this.$emit('click', foo)
}
// 使用了装饰器后
// onFooClick的参数foo 是$emit的第二个参数
@Emit('click')
onFooClick(foo: any) {}

但是如果foo的来源其实是组件内部的响应式数据,原本的写法只需要将参数去掉,$emit的第二个参数改为this.foo,即:

1
2
3
4
5
6
7
8
// 按照原本vue的写法
onFooClick() {
this.$emit('click', this.foo)
}
// 使用了装饰器后
// 需要在调用onFooClick的地方将this.foo作为参数传给onFooClick
@Emit('click')
onFooClick(foo: any) {}

并且,@Prop这个装饰器并没有解决Vue自带的类型检查和typescript类型检查的矛盾,该装饰器依然可以接受vue类型的写法。
这意味着如果你想进行静态类型检查+运行时的类型检查,需要写两套规则不太一样的类型。

1
2
@Prop([String, Boolean])
someProp: string | boolean

Declaration

Vue有不少插件会在Vue实例下面挂在新的方法,而官方提供的Vue类型生命中是肯定没有这些方法的,为了解决这个问题,Vue官方推荐使用module augmentation来帮助Vue补充新的类型声明。
从工程上看,这个举动确实能帮助类型检查,但是回到现实情况中,一般我们会在项目的入口文件中给vue注入挂载各种插件(即在index.js或者main.js之类的入口文件调用Vue.use()),基本不会存在调用的时候Vue没有挂载该方法的情况,然而Vue下挂载属性一般是将其用作全局状态来使用的,同时vue也提供vuex这样的全局状态管理库,直接把状态挂载vue实例下的情况并不多见,再加上如果是服务端渲染的项目,这类side effect更应该交由vuex来管理。不过话说回来,防范于未然总是好的。
上面这句话不一定正确,先划掉。

Reactive

响应式的数据是vue的特色之一,但是也导致了使用了响应式数据的计算属性/方法无法有typescript来推断类型,需要自己手动标注。不能使用类型推断而需要自己一个个手动标注,如果是比较大的项目,很有可能出现很多变量都是any类型(是的我就喜欢这样干),那这样引入ts也就毫无意义了

简单总结

随着vue版本更新,vue项目也能ts带来的诸多好处,但是并不是每一个团队都能在紧张刺激的产品迭代期间挤出时间进行这样的重构。相比这些好处,重构的工程量究竟会有多大,是否真的有必要这样做是一个值得思考的问题。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2018 香香鸡的小窝 All Rights Reserved.

SunskyXH hold copyright