前言
在 vue-cli 3.0 的脚手架出来以后,官方我们提供了一套 Vue 的 TypeScript 模板,解决了许多模块以及类型问题,官方的东西真香,因此可以使用 TypeScript 搞一波事情。
基础配置
配置默认是全家桶,其中预处理器建议使用 less
,如果使用 sass
可能会因为各种莫名其妙的原因安装不上 node-sass
;babel
也是必选的,目的是将 TypeScript 编译后的代码转变成 ES5 的代码,提供低版本浏览器支持。
VScode 的插件配置,基本上安装 TypeScript Extension Pack
这个插件以后附带的几个插件够用了(我是一个强迫症,能少安装插件就尽量少安装插件)。然后需要额外安装一个 TSlint Vue
插件,因为 VScode 对 .vue 单文件的支持并不是很好,TSlint 不能有效纠错,需要这个插件配合。
这是生成的默认配置。其中 tsconfig.json
里会设置 src/xxx
的别名为 @/xxx
,但是 VScode 是不能识别的,所以需要自行新建一个 jsconfig.json
文件。
// jsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"module": "commonjs",
"paths": {
"@/*":["./src/*"]
}
}
}
开始开发
基础使用
import { Vue, Component, Prop, Watch } from 'vue-property-decorator'
import { State, Getter, Mutation, Action } from 'vuex-class'
import { Bind, Debounce } from 'lodash-decorators'
import { UBT } from '@/decorator'
import HelloWorld from '@/components/HelloWorld.vue'
import BasicMixin from '@/mixin/PrintMixin'
@Component({
components: {
HelloWorld
},
mixins: [BasicMixin]
})
export default class EleComponent extends Vue {
@Prop({ default: 'Hello' })
private text!: string
@State(state => state.user)
private user!: string
@Getter('email')
private email!: string
private msg: string = 'Hello Element'
private name: string = 'Typescript'
private get userInfo (): string {
return this.text + this.name
}
private set userInfo (val: string) {
this.text = val
}
@Mutation('setUserEmail')
private setUserEmail!: (email: string) => void
@Action('getUserInfo')
private getUserInfo!: (params: {token: string}) => Promise<any>
@Watch('name', { deep: true })
private onNameChange () {
console.log('name has been changed!')
}
@UBT('click', Date.now())
@Bind()
@Debounce(300)
private handleClick () {
console.log('click', this.text)
}
}
template
和 style
部分和普通的 js 差不多,这里只贴出 script
部分的代码。基本上就是把传统的配置对象改为了基于 class 的组件,传递的 props、watch、computed 以及 Vuex 的相关属性都通过装饰器实现。
vue-property-decorator 提供 Vue 基本属性的实现。注意一定要使用 @compoenet
去修饰这个组件,否则其它的装饰器无法正常使用。@component({option})
中接收的参数 option 就是传统的配置,mixin 和子组件的注册都要在这里声明。
vuex-class 提供的是与 Vuex 相关的装饰器,具体用法参考文档。美中不足的是,Store 的定义还是基于配置的,因此 TypeScript 无法正确推导出其方法的签名,并且通过装饰器在组件中声明的方法也是没有签名,所以在组件中需要自行补上方法的签名。
最后一部分实现了一个方法 handleClick
并且使用了三个装饰器进行修饰。主要的目的是实现点击事件的防抖,lodash-decorators 提供了相关的装饰器。传统的 vue 组件如果需要实现的一个防抖事件需要这样写
{
methods: {
debounceClick: _.debounce(this.handleClick, 300)
}
}
这样做是为了 this 指向正确,Vue 会自动为 methods 中的方法绑定 this,但是这样的实现既不优雅也不通用,基于 class 的组件我们只需要 Bind
和 Debounce
两个装饰器就能完成,并且在 React 中也是通用的
使用 Mixin
mixin 在 Vue 中使用到的场景很多,其目的是在组件中复用相同的功能代码,但是这种实现并不优雅,它仅仅是功能上实现复用,结构上并没有拓展功能,并且会破坏组件原有的结构,特别是基于 class 的组件。不过传统的 Vue 组件使用 JavaScript 这种类型推断本来就没有,所以显得不重要。希望 Vue 3.0也能像 React 一样实现通过 HOC 复用代码。在 TypeScript 中,不能再像原来一样写基于配置的 mixin 对象,而应该也写为一个 Vue 的子类:
import { Vue, Component } from 'vue-property-decorator'
export default class BasicMixin extends Vue {
private msg!: string
private printWords (): void {
console.log(this.msg)
}
}
在需要的注入的组件中通过 @component
注入,需要注意的是,如果注入的 class 需要使用被注入组件的属性,需要通过 priavte msg!: string
强制断言属性存在,才能正常使用;同理,如果组件需要使用注入类的方法,也要强制断言。如果只是 template 中使用方法,那么不需要强制断言
填坑指南
- VScode 插件配置 TSLint Vue
- mixin 的相关配置
- Vuex 方法的接口实现
- 复用接口的摆放位置
使用了 TypeScript 以后必然需要声明许多接口。个人觉得有必要定义的接口有:- 后台返回的数据结构,这样能够避免每次都打开 network 看返回的数据结构格式;
- 组件内部复用的数据结构,一些数据结构是前端生成的并且在多个组件复用,这些需要提取出来写成接口;
在接口文件存储的位置上一般分为两类: - 统一定义在
@/interface
通用的接口提取出来放到这个地方; - API 请求文件中,我按照页面的粒度分离了请求 API 的方法,页面级的接口文件也定义在这里,这样在导入请求方法时也可以同时导入接口声明;
- get set 的使用
TypeScript 中不再使用computed
定义计算属性,而是通过 class 本身的 get set 定义,使用的方式和原来相同 - 路由的组件导航守卫失效
路由的导航钩子不属于 Vue 本身,这会导致 class 组件转义到配置对象时导航钩子无效,因此如果要使用导航钩子需要在 router 的配置里声明 - axios 填坑
使用 axios 请求数据时,它会将数据再包裹一个 AxiosPromise 的接口,具体实现是
export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}
export interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request?: any;
}
通常我们会在 axios.interceptors.response.use
这个拦截方法中取出 res.data,但是这样会导致 axios 返回数据的类型推断失败(即使取出来了,axios 还是会认为返回的是 AxiosPromise ),因此需要这样定义:
export const GET_CITY = (form: IQuery, { target, page, rows }: IParams): Promise<IRes<IRankList>> => {
const query = Object.assign({}, form, { target, page, rows })
return ajax.post(URL.city, query).then(res => res.data)
}
const res: IRes<IRankList> = await GET_CITY()
每个返回的结构都需要手动 then(res => res.data)
这样返回的才是 Promise<T>
, await
才能正确的取出结构
总结
目前看来 Vue 对 TyepeScript 的支持并不算完善,因为 2.x 版本的 Vue 即使写了 class-compoent,最终也会被编译成配置式的组件。许多 Vue 中方便的 API 以及 Vuex 的方法也只能通过装饰器实现,这导致了方法签名的丢失;通过 ref 属性获取到的子组件实例的类型也不正确,只是一个普通的 Vue 实例并不是定义的 class 类型(在组件内部通过 private public 定义的方法,父组件调用时是无法使用的,React 则实现了这个功能);子组件需要的参数声明也不具有强制性,参考 React 组件参数传递是具有强约束力并且能静态检测,目前 Vue 仍然是在运行时抛出
不过好消息是,Vue 3.0 将采用 TypeScript 重构,全新的 Vue 不仅带来性能上的提升,还会进一步提升对类型的支持。未来,class-compoent 也将成为主流,现在写 TypeScript 以后进行 3.0 的迁移会更加方便。